Mengukur memberi kita angka, tetapi angka belum menunjuk kolom mana yang boros. Profiler mengisi kekosongan itu: alat ini merekam eksekusi program lalu menyusun peta waktu dan memori ke tiap function, sehingga kita berhenti menebak dan mulai melihat. Node.js membawa profiler V8 tanpa perlu memasang apa pun — cukup flag pada saat menjalankan proses. Chapter ini membahas dua keluarga profiler, cara menjalankan profil CPU dan heap lewat flag bawaan, serta cara membaca hasilnya di Chrome DevTools.
Dua Keluarga Profiler #
| Aspek | Instrumentasi | Sampling |
|---|---|---|
| Cara kerja | Membungkus kode dengan pencatat (counter, timer) | Mesin secara berkala memotret call stack yang sedang berjalan |
| Hasil | Hitungan pasti: berapa kali tiap function dipanggil | Statistik: perkiraan persentase waktu di tiap function |
| Overhead | Sebanding jumlah pemanggilan — bisa besar (efek pengamat dari chapter sebelumnya) | Kecil dan hampir datar, cocok untuk proses produksi |
| Kegunaan | Menghitung frekuensi kejadian | Menemukan function yang paling banyak memakan CPU |
Instrumentasi menjawab “berapa kali”, sampling menjawab “berapa lama”. Keduanya saling melengkapi, dan kita akan memakai keduanya di chapter ini: instrumentasi dulu untuk peta kasar, lalu sampling untuk menemukan titik panas yang sesungguhnya.
Peta Awal dengan Penghitung Panggilan #
Program berikut merakit laporan dari data uji tetap. Tiga function kuncinya kita bungkus dengan lacak(), sebuah instrumen yang mencatat setiap pemanggilan ke sebuah Map:
import { createHash } from "node:crypto";
// Instrumen sederhana: pembungkus yang menghitung pemanggilan.
const statistik = new Map();
function lacak(nama, fn) {
return (...args) => {
statistik.set(nama, (statistik.get(nama) ?? 0) + 1);
return fn(...args);
};
}
const renderBaris = lacak("renderBaris", (sel) =>
sel.map((b) => b.toString(16).padStart(2, "0")).join(" ")
);
const renderTabel = lacak("renderTabel", (baris) =>
baris.map((sel) => renderBaris(sel)).join("\n")
);
const buatChecksum = lacak("buatChecksum", (data) => {
const hash = createHash("sha256");
for (let i = 0; i < 50000; i++) {
hash.update(data);
}
return hash.digest("hex").slice(0, 12);
});
const dataUji = [];
for (let baris = 0; baris < 40; baris++) {
dataUji.push(Array.from({ length: 8 }, (_, kolom) => (baris * 8 + kolom) % 256));
}
const hasil = [];
for (let ulang = 0; ulang < 3; ulang++) {
hasil.push(renderTabel(dataUji));
hasil.push(buatChecksum(`laporan-${ulang}`));
}
console.log("jumlah pemanggilan tiap function:");
for (const [nama, jumlah] of [...statistik.entries()].sort((a, b) => b[1] - a[1])) {
console.log(` ${nama.padEnd(14)} ${String(jumlah).padStart(4)} kali`);
}
console.log("pekerjaan internal buatChecksum: 3 x 50000 = 150000 hash.update");
console.log("panjang laporan tercetak:", hasil.join("\n").length, "karakter");Output yang Dihasilkan:
jumlah pemanggilan tiap function:
renderBaris 120 kali
renderTabel 3 kali
buatChecksum 3 kali
pekerjaan internal buatChecksum: 3 x 50000 = 150000 hash.update
panjang laporan tercetak: 2918 karakterBedah baris-per-baris untuk bagian intinya:
- Baris 4-11 (
statistikdanlacak):Mapmenyimpan nama function sebagai kunci dan jumlah pemanggilan sebagai nilai;lacak()mengembalikan function pembungkus yang menaikkan hitungan sebelum meneruskan pemanggilan asli lewatfn(...args). - Baris 13-15 (
renderBaris): function kecil yang mengubah satu baris data menjadi teks heksadesimal — ini yang paling sering dipanggil, 120 kali (40 baris × 3 putaran). - Baris 17-19 (
renderTabel): memanggilrenderBarisuntuk tiap baris; pembungkusannya tercatat 3 kali. - Baris 21-27 (
buatChecksum): menghitung hash SHA-256 dengan 50.000 kalihash.update()per pemanggilan. Hanya dipanggil 3 kali, tetapi perhatikan baris keempat output — pekerjaan internalnya mencapai 150.000 operasi. - Baris 34-38 (loop utama): tiga putaran perakitan laporan menghasilkan pola pemanggilan yang selalu identik karena seluruh beban ditentukan oleh
dataUjiyang tetap.
Di sinilah batas penghitung pemanggilan: renderBaris unggul 40 banding 1 dari buatChecksum dalam frekuensi, tetapi hampir seluruh CPU sesungguhnya habis di dalam buatChecksum. Jumlah pemanggilan bukanlah waktu — untuk mengukur waktu kita perlu sampling.
Profil CPU dengan –cpu-prof #
Flag --cpu-prof
memerintahkan V8 merekam sampling profiler sejak proses dimulai sampai proses keluar:
node --cpu-prof hitung-panggilan.mjsSaat proses berakhir, V8 menulis berkas profil ke direktori kerja dengan pola nama CPU.TANGGAL.JAM.PID.NOMOR.cpuprofile — misalnya CPU.20260115.080000.1234.0.001.cpuprofile. Berkas ini berformat JSON berisi daftar sampel call stack. Buka Chrome atau Edge, kunjungi halaman chrome://inspect, pilih Performance (atau Memory pada beberapa versi) lalu muat berkas .cpuprofile tersebut — DevTools merendernya sebagai flame graph.
Cara membaca flame graph-nya:
- Sumbu horizontal adalah waktu; lebar sebuah balok sebanding dengan porsi sampel yang jatuh pada function itu — balok terlebar adalah kandidat titik panas pertama.
- Sumbu vertikal adalah kedalaman call stack; function di atas adalah pemanggil, function di bawah adalah yang dipanggil.
- Klik sebuah balok untuk memperbesar bagian tersebut; bandingkan lebar balok anak terhadap induknya untuk melihat berapa porsi waktu induk yang habis di anak itu.
- Pada contoh laporan kita, balok
buatChecksumakan tampil sangat lebar meski jarang dipanggil — persis kebalikan dari tabel hitungan pemanggilan.
–prof dan –prof-process #
Varian lawas namun masih berguna adalah flag --prof, yang memakai tick processor V8. Jalankan program dengan flag ini, beri beban, lalu olah berkas lognya:
node --prof server.mjs
node --prof-process isolate-0x12340000-5678-v8.log > ringkasan.txtBerkas isolate-...-v8.log muncul di direktori kerja setelah proses berakhir, dan --prof-process mengubahnya menjadi ringkasan teks. Bagian [Summary] membagi sampel menjadi beberapa kelompok — JavaScript, C++, GC, dan shared libraries — sehingga kita langsung tahu apakah waktu habis di kode JS kita, di native code, atau justru di garbage collector. Panduan resmi Profiling Node.js Applications
memuat tutorial lengkap alur ini, termasuk cara membaca bagian [C++] untuk menemukan function native paling boros.
Profil Heap dengan –heap-prof dan Heap Snapshot #
CPU bukan satu-satunya sumber kelambatan; memori yang bocor membuat GC bekerja makin keras hingga aplikasi berhenti total. Flag --heap-prof
merekam di mana alokasi memori terjadi secara sampling, lalu menulis berkas .heapprofile yang juga bisa dimuat di Chrome DevTools — panel Memory menampilkan pohon alokasi, dan cabang terlebar adalah pengalokasi terbesar.
Untuk memotret isi heap pada satu titik waktu, kita memakai heap snapshot: jalankan proses dengan node --inspect, sambungkan DevTools seperti pada chapter Debugging dengan node –inspect, lalu tombol Take snapshot pada panel Memory membekukan seluruh object yang hidup saat itu. Trik klasiknya adalah memotret dua kali — sekali sebelum dan sekali setelah kejadian yang dicurigai — lalu memakai mode perbandingan untuk melihat object mana yang bertambah dan tidak pernah kembali. Panduan resmi Using Heap Snapshot
memandu alurnya langkah demi langkah.
Flame graph memberi tahu waktu CPU habis di mana, dan profil heap memberi tahu memori tertahan oleh siapa. Dua pertanyaan itulah yang perlu kita jawab sebelum menyentuh satu baris kode optimasi. Untuk kasus memori, kita butuh pemahaman dasar tentang cara V8 mengelola heap — pembagian generasi, promosi object, dan kapan GC justru menjadi penyebab kelambatan. Itulah topik chapter Memory Management dan Garbage Collection.