Aplikasi terasa lambat itu keluhan, bukan diagnosis. Sebelum kita bisa memperbaiki performa, kita perlu menjawab tiga pertanyaan dengan angka: apa yang lambat, lambat dibandingkan dengan apa, dan apakah perubahan kita benar-benar memperbaikinya. Chapter ini membahas metrik yang lazim diukur, cara merancang alat ukur yang hasilnya bisa dipercaya, dan jebakan yang muncul justru karena akt mengukur itu sendiri. Semua konsep di sini akan menjadi fondasi bagi chapter profiling, caching, hingga load testing di sepanjang Section ini.
Metrik yang Perlu Kita Kenal #
Empat metrik berikut menutup hampir semua percakapan tentang performa aplikasi Node.js:
| Metrik | Pertanyaan yang dijawab | Satuan lazim |
|---|---|---|
| Latency | Berapa lama satu operasi selesai dari sudut pandang pemakai? | milidetik, sering dilaporkan sebagai persentil p50/p95/p99 |
| Throughput | Berapa banyak operasi selesai per satuan waktu? | request per second (RPS) |
| Memori | Berapa banyak RAM yang dipakai, dan apakah angkanya stabil? | byte, atau MB |
| Event loop lag | Seberapa lama event loop tertahan dari satu pekerjaan berat? | milidetik |
Latency dan throughput sering tarik-menarik: menambah antrian bisa menaikkan throughput tetapi memperburuk latency. Memori yang terus tumbuh adalah pertanda kebocoran yang akan kita bedah pada chapter Memory Management dan Garbage Collection. Sementara event loop lag adalah penyakit khas Node.js yang sudah kita kenali pada Section Asynchronous Runtime — main thread yang sibuk menghitung membuat semua request ikut menunggu.
Menghitung Langkah, Bukan Sekadar Mendetik #
Cara paling jujur untuk membandingkan dua algoritma bukan mencatat detik, melainkan menghitung unit kerja yang benar-benar dikerjakan. Waktu eksekusi berubah-ubah mengikuti mesin dan beban CPU, tetapi “jumlah langkah” untuk beban yang sama selalu identik — inilah inti cost model yang dipakai analisis kompleksitas. Contoh berikut membandingkan tiga strategi pencarian pada 10.000 data, menghitung setiap langkahnya, lalu mencatat “waktu” melalui stopwatch palsu yang angkanya kami suntikkan secara tetap:
let langkah = 0;
function cariLinear(daftar, target) {
for (let i = 0; i < daftar.length; i++) {
langkah += 1;
if (daftar[i] === target) return i;
}
return -1;
}
function cariBiner(daftarUrut, target) {
let rendah = 0;
let tinggi = daftarUrut.length - 1;
while (rendah <= tinggi) {
langkah += 1;
const tengah = Math.floor((rendah + tinggi) / 2);
if (daftarUrut[tengah] === target) return tengah;
if (daftarUrut[tengah] < target) rendah = tengah + 1;
else tinggi = tengah - 1;
}
return -1;
}
function buatIndeks(daftar) {
const peta = new Map();
daftar.forEach((nilai, i) => peta.set(nilai, i));
return {
cari(target) {
langkah += 1;
return peta.get(target) ?? -1;
},
};
}
// Stopwatch palsu: mengembalikan durasi dari daftar tetap,
// bukan dari jam sungguhan. Angka hasil pengukuran jadi stabil.
function buatStopwatchPalsu(daftarMs) {
let indeks = 0;
return {
ukur(fn) {
fn();
const ms = daftarMs[indeks];
indeks += 1;
return ms;
},
};
}
const kodeProduk = [];
for (let i = 0; i < 10000; i++) {
kodeProduk.push(`SKU-${String(i).padStart(5, "0")}`);
}
const target = "SKU-09999";
const indeks = buatIndeks(kodeProduk);
let posisi = -1;
const stopwatch = buatStopwatchPalsu([0.19, 0.002, 0.0001]);
const varian = [
{ nama: "linear", jalankan: () => (posisi = cariLinear(kodeProduk, target)) },
{ nama: "biner", jalankan: () => (posisi = cariBiner(kodeProduk, target)) },
{ nama: "indeks", jalankan: () => (posisi = indeks.cari(target)) },
];
console.log("strategi langkah waktu (jam palsu)");
for (const { nama, jalankan } of varian) {
langkah = 0;
const ms = stopwatch.ukur(jalankan);
console.log(
`${nama.padEnd(9)} ${String(langkah).padStart(6)} ${ms} ms (posisi ${posisi})`
);
}Output yang Dihasilkan:
strategi langkah waktu (jam palsu)
linear 10000 0.19 ms (posisi 9999)
biner 14 0.002 ms (posisi 9999)
indeks 1 0.0001 ms (posisi 9999)Mari kita bedah bagian-bagian pentingnya:
- Baris 1 (
let langkah = 0): counter global menjadi “alat ukur” kita — setiap strategi menambahnya setiap kali melakukan satu unit kerja, sehingga hasil hitungannya pasti sama di komputer mana pun. - Baris 3-9 (
cariLinear): pencarian lurus memeriksa data satu per satu; target di posisi terakhir memaksa 10.000 langkah penuh. - Baris 11-22 (
cariBiner): pencarian biner membuang setengah sisa data setiap langkah pada daftar terurut — 10.000 data cukup 14 langkah, sesuai kompleksitas O(log n). - Baris 24-33 (
buatIndeks):Mapdibangun sekali di luar pengukuran, lalu tiap pencarian hanya satu langkah lihat-lihat (O(1)); builder-nya sengaja tidak ikut dihitung karena biaya awal itu dibayar sekali. - Baris 35-47 (
buatStopwatchPalsu): stopwatch menerima daftar milidetik tetap, bukan membaca jam asli. Injeksi seperti ini membuat laporan benchmark stabil — trik yang sama akan kita pakai lagi di chapter Caching untuk jam TTL. - Baris 65-71 (loop laporan): sebelum tiap varian,
langkahdi-nol-kan;stopwatch.ukur(jalankan)menjalankan pekerjaannya lalu mengembalikan angka dari daftar tetap.
Di dunia nyata kita mengganti stopwatch palsu dengan jam sungguhan dari perf_hooks, tanpa mengubah struktur harness sama sekali:
import { performance } from "node:perf_hooks";
const stopwatchNyata = {
ukur(fn) {
const mulai = performance.now();
fn();
return performance.now() - mulai;
},
};Method performance.now()
mengembalikan waktu berlalu dengan presisi pecahan milidetik. Angka hasilnya berbeda-beda di setiap eksekusi — itulah mengapa contoh tadi memakai jam palsu, agar output buku ini dapat diverifikasi ulang secara identik. Prinsip yang bisa dibawa pulang: pisahkan apa yang diukur (pekerjaan) dari alat pengukurnya (jam), sehingga alat ukur bisa ditukar tanpa menulis ulang benchmark.
Efek Pengamat #
Mengukur selalu mengubah hasil pengukuran — fisikawan menyebutnya observer effect, dan hal yang sama terjadi pada benchmark. Kode pencatat waktu, penghitung langkah, dan profiler semuanya berjalan di CPU yang sama dengan kode yang diukur. Demo berikut membuat efek itu terlihat dalam bentuk angka yang bisa dihitung:
// Bandingkan dua program yang pekerjaannya sama persis.
// Satu mengukur pekerjaan murni, satu lagi mengukur
// pekerjaan + alat ukurnya sendiri (efek pengamat).
let langkah = 0;
function satuUnitKerja() {
langkah += 1;
}
function ukurTanpaInstrumen() {
langkah = 0;
for (let i = 0; i < 1000; i++) {
satuUnitKerja();
}
return langkah;
}
function ukurDenganInstrumen() {
langkah = 0;
const instrumen = () => {
langkah += 1; // biaya mencatat ikut terhitung sebagai langkah
satuUnitKerja();
};
for (let i = 0; i < 1000; i++) {
instrumen();
}
return langkah;
}
const murni = ukurTanpaInstrumen();
const terukur = ukurDenganInstrumen();
console.log("langkah tanpa alat ukur :", murni);
console.log("langkah dengan alat ukur:", terukur);
console.log("selisih:", terukur - murni, "langkah adalah biaya pengukuran");Output yang Dihasilkan:
langkah tanpa alat ukur : 1000
langkah dengan alat ukur: 2000
selisih: 1000 langkah adalah biaya pengukuranBedah singkatnya: satuUnitKerja() menambah langkah satu kali per pemanggilan, sedangkan instrumen() menambah langkah dua kali — sekali untuk mencatat, sekali untuk pekerjaan. Setelah 1.000 putaran, versi terukur menunjukkan 2.000 langkah: 100% lebih besar dari pekerjaan murninya. Konsekuensi praktisnya sederhana namun sering dilupakan:
Micro Benchmark vs Macro Benchmark #
| Aspek | Micro benchmark | Macro benchmark |
|---|---|---|
| Yang diukur | Satu function dalam isolasi | Jalur lengkap satu operasi, misalnya satu request HTTP |
| Contoh | cariLinear vs indeks.cari | Latency GET /tugas dari koneksi hingga respons |
| Kegunaan | Memilih algoritma dan struktur data | Memvalidasi arsitektur dan pengalaman pemakai |
| Jebakan | Hilangnya biaya nyata: jaringan, serialisasi, antrean | Sulit menyimpulkan penyebab bila banyak komponen terlibat |
Micro benchmark menolong kita memilih potongan yang tepat, tetapi aplikasi tidak terasa lebih cepat kalau potongan itu bukan yang mendominasi waktu. Macro benchmark mendekati pengalaman pemakai, tetapi butuh beban yang menyerupai produksi — di sanalah load testing masuk, dan akan kita kerjakan pada chapter terakhir Section ini.
Alat Ukur Bawaan: perf_hooks #
Node.js membawa modul node:perf_hooks yang menyediakan jam berketelitian tinggi plus kerangka penanda mark dan measure bergaya web standard. Pola lengkapnya seperti berikut:
import { performance, PerformanceObserver } from "node:perf_hooks";
const pengamat = new PerformanceObserver((daftar) => {
for (const entri of daftar.getEntries()) {
console.log(`${entri.name}: ${entri.duration} ms`);
}
pengamat.disconnect();
});
pengamat.observe({ entryTypes: ["measure"] });
performance.mark("mulai-render");
// ... pekerjaan yang ingin diukur ...
performance.mark("selesai-render");
performance.measure("render", "mulai-render", "selesai-render");performance.mark()
menancapkan penanda bernama pada garis waktu, performance.measure()
menghitung jarak dua penanda, dan PerformanceObserver
menerima hasilnya secara asynchronous setelah pengukuran selesai — properti entri.duration menyimpan durasinya. Kami sengaja tidak mencantumkan output contoh ini karena durasi sungguhan berbeda di setiap eksekusi; struktur kodenyalah yang penting kita tiru. Untuk mengamati seberapa banyak waktu yang benar-benar dihabiskan tiap function, kita butuh alat yang lebih tajam: profiler — topik chapter berikutnya.