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:

MetrikPertanyaan yang dijawabSatuan lazim
LatencyBerapa lama satu operasi selesai dari sudut pandang pemakai?milidetik, sering dilaporkan sebagai persentil p50/p95/p99
ThroughputBerapa banyak operasi selesai per satuan waktu?request per second (RPS)
MemoriBerapa banyak RAM yang dipakai, dan apakah angkanya stabil?byte, atau MB
Event loop lagSeberapa 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:

js
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:

text
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:

  1. 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.
  2. Baris 3-9 (cariLinear): pencarian lurus memeriksa data satu per satu; target di posisi terakhir memaksa 10.000 langkah penuh.
  3. 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).
  4. Baris 24-33 (buatIndeks): Map dibangun 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.
  5. 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.
  6. Baris 65-71 (loop laporan): sebelum tiap varian, langkah di-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:

js Stopwatch nyata memakai performance.now()
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:

js
// 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:

text
langkah tanpa alat ukur : 1000
langkah dengan alat ukur: 2000
selisih: 1000 langkah adalah biaya pengukuran

Bedah 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 #

AspekMicro benchmarkMacro benchmark
Yang diukurSatu function dalam isolasiJalur lengkap satu operasi, misalnya satu request HTTP
ContohcariLinear vs indeks.cariLatency GET /tugas dari koneksi hingga respons
KegunaanMemilih algoritma dan struktur dataMemvalidasi arsitektur dan pengalaman pemakai
JebakanHilangnya biaya nyata: jaringan, serialisasi, antreanSulit 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:

js Pola performance.mark dan PerformanceObserver
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.