CPU-profiling en flamegraphs voor hot paths
Leg CPU-profielen vast en lees flamegraphs om functies te vinden die de meeste tijd verbruiken.
CPU-profiling en flamegraphs voor hot paths is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met Node.js. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.
Waarom CPU-profiling belangrijk is
Wanneer een Node.js-backend traag aanvoelt, is de oorzaak meestal een van twee dingen: het proces is aan het wachten (I/O, database, netwerk) of het is aan het rekenen (CPU verbruiken op de enkele hoofdthread). Een CPU-profiel vertelt je precies waar de tweede soort tijd naartoe gaat.
- Het neemt met een vaste frequentie monsters van de aanroepstack (V8 gebruikt ongeveer 1000 Hz, één monster per milliseconde).
- Elk monster legt vast welke functie momenteel wordt uitgevoerd en de volledige stack van aanroepers.
- Functies die in veel monsters voorkomen, zijn je hete paden — de code die het optimaliseren waard is.
Omdat Node.js JavaScript op één thread uitvoert, kan één hete functie elke binnenkomende aanvraag blokkeren. Profiling vindt die functie voor je, in plaats van dat je moet gokken.
Eigen tijd versus totale tijd
Elke profiler onderscheidt twee getallen per functie, en ze door elkaar halen is de meest voorkomende profilingfout.
- Eigen tijd (ook wel exclusief): de tijd die wordt besteed aan het uitvoeren van de body van de functie zelf, exclusief de onderliggende functies die deze aanroept.
- Totale tijd (ook wel inclusief): eigen tijd plus alle tijd die in aangeroepen functies wordt besteed.
Een functie met veel totale tijd kan alleen maar een coördinator zijn die dure onderliggende functies aanroept. De functie met veel eigen tijd is waar de CPU daadwerkelijk wordt belast. Optimaliseer eerst op basis van eigen tijd.
Een profiel opnemen vanaf de CLI
De snelste manier om een profiel van een script te maken is de ingebouwde V8-vlag. Geen extra pakketten en geen codewijzigingen.
node --prof app.jsschrijft een onbewerktisolate-*.log-bestand.node --prof-process isolate-*.log > profile.txtzet dit om in een voor mensen leesbare samenvatting met een uitsplitsing van de ticks.
De samenvatting groepeert ticks per JavaScript, C++ en GC en vermeldt de zwaarste functies. De uitvoer bestaat alleen uit tekst, dus gebruiken we hierna het inspector-protocol voor visuele vlamgrafieken. Hieronder staat een CPU-intensieve werklast die je op deze manier kunt profileren.
function isPrime(n) {
if (n < 2) return false;
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false;
}
return true;
}
function countPrimes(limit) {
let count = 0;
for (let n = 0; n < limit; n++) {
if (isPrime(n)) count++;
}
return count;
}
console.log(countPrimes(2_000_000));Programatisch opnemen met de inspector
Voor een langdurig actieve server wil je vaak een specifiek tijdvenster profileren. Met de ingebouwde module inspector kun je de V8-CPU-profiler vanuit je code starten en stoppen en een .cpuprofile-bestand opslaan.
- Open een
Session, maak verbinding en schakel het domeinProfilerin. - Roep
Profiler.startaan, voer de werklast uit en roep daarnaProfiler.stopaan. - Het geretourneerde profiel is JSON dat je naar schijf schrijft en in Chrome DevTools of VS Code laadt.
const inspector = require('node:inspector');
const fs = require('node:fs');
const session = new inspector.Session();
session.connect();
function work() {
let sum = 0;
for (let i = 0; i < 5e7; i++) sum += Math.sqrt(i);
return sum;
}
session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
work();
session.post('Profiler.stop', (err, { profile }) => {
fs.writeFileSync('./work.cpuprofile', JSON.stringify(profile));
console.log('Saved work.cpuprofile');
session.disconnect();
});
});
});Wat een vlamgrafiek echt laat zien
Een flame graph zet de stackmonsters om in een afbeelding. Je leest die zo:
- De x-as is GEEN tijd — het is de verzameling stacks. Breedte = hoeveel monsters dat frame bevatten, oftewel hoeveel CPU het gebruikte.
- De y-as is de stackdiepte. Het frame onderaan is de aanroeper; frames die erbovenop staan, zijn de aangeroepen functies.
- Een breed frame betekent dat een functie (en de onderliggende functies) veel CPU gebruikte. Brede frames bovenaan met weinig erboven zijn de bladeren die het echte werk doen.
Kleuren zijn meestal willekeurig en hebben geen betekenis — verbind er geen conclusies aan. Zoek naar de breedste plateaus, niet naar de hoogste torens.
Flame graph versus flame chart
Deze zien er vergelijkbaar uit, maar beantwoorden verschillende vragen, en Chrome DevTools toont ze allebei.
- Flame chart (de tijdlijn "Performance" van DevTools): de x-as is verstreken kloktijd, van links naar rechts. Handig om te zien wanneer iets gebeurde en in welke volgorde gebeurtenissen plaatsvonden.
- Flame graph (geaggregeerd): identieke frames worden samengevoegd en op breedte gesorteerd. Handig om te zien welke functie tijdens de hele uitvoering zwaar belast werd, ongeacht wanneer die liep.
Om drukke paden te vinden heb je de geaggregeerde flame graph nodig: een functie die 10.000 keer op verspreide momenten wordt aangeroepen, verschijnt als één dikke balk in plaats van als 10.000 onzichtbaar dunne reepjes.
Flame graphs genereren met 0x
De tool 0x start je proces, legt een profiel vast en maakt in één stap een interactieve HTML-flame graph — ideaal voor Node.js-services.
npx 0x app.jsvoert de app uit en opent bij het afsluiten een flame graph in de browser.- Stuur bij een server belasting naar de server (bijvoorbeeld met
autocannon) terwijl0xregistreert en stop daarna het proces om de graph te genereren.
In de viewer van 0x kun je op elk frame klikken om in te zoomen en op naam zoeken om elke plek waar een functie voorkomt te markeren. Hieronder staat een kleine HTTP-server die de moeite waard is om onder belasting te profileren.
const http = require('node:http');
function renderRow(i) {
return '<tr><td>' + i + '</td><td>' + (i * i) + '</td></tr>';
}
http.createServer((req, res) => {
let html = '<table>';
for (let i = 0; i < 5000; i++) {
html += renderRow(i);
}
html += '</table>';
res.setHeader('Content-Type', 'text/html');
res.end(html);
}).listen(3000, () => console.log('listening on 3000'));De graph lezen: vind het breedste blad
Een gedisciplineerde manier om in elke flame graph het drukke pad te vinden:
- Bekijk de bovenrand van de graph (de bladframes). Dit zijn de functies die daadwerkelijk actief waren toen de monsters werden genomen.
- Zoek het breedste blad of plateau. Dat ene frame is je grootste hoeveelheid eigen tijd.
- Volg vanaf dat frame de lijn omlaag om de aanroe keten te zien die daarheen leidt — zo weet je wat je moet aanpassen.
Pas op voor ruis van frameworks: frames zoals (anonymous), module.exports of interne runtimeframes zijn vaak breed omdat alles erdoorheen loopt. Negeer brede orchestrator-frames en richt je op brede blad-frames.
Druk op garbagecollection herkennen
Flame graphs onthullen niet alleen je code — ze laten ook de V8-runtime zien. Als je brede frames ziet met namen als GC, Scavenge of Mark-Compact, wordt de CPU gebruikt om geheugen op te ruimen en niet om logica uit te voeren.
- Een grote GC-breedte betekent meestal dat je op het drukke pad te veel kortlevende objecten maakt (tekenreeksen samenvoegen in lussen, of per aanvraag closures of arrays maken).
- De oplossing is zelden "de functie optimaliseren" — het is "minder toewijzen": hergebruik buffers, reserveer arrays vooraf en vermijd objectliteralen per iteratie.
Het voorbeeld hieronder maakt bij elke iteratie een nieuw object: het klassieke patroon waardoor GC-frames oplichten.
function process(n) {
const results = [];
for (let i = 0; i < n; i++) {
// a new object every iteration -> GC pressure
results.push({ id: i, squared: i * i, label: 'item-' + i });
}
let sum = 0;
for (const r of results) sum += r.squared;
return sum;
}
console.log(process(1_000_000));Aanwijzingen voor deoptimalisatie en inline plaatsen
V8 compileert druk gebruikte functies naar geoptimaliseerde machinecode. Wanneer een functie terug moet naar tragere bytecode, is die gedeoptimaliseerd; dat verschijnt als onverwacht brede frames.
- Voer
node --trace-deopt app.jsuit om elke deoptimalisatie met de reden te loggen (bijvoorbeeld veranderende objectvormen of gemengde typen in een array). node --trace-opt app.jstoont welke functies zijn geoptimaliseerd.- Als functieargumenten monomorf blijven (altijd dezelfde vorm en hetzelfde type), kan V8 ze geoptimaliseerd en inline gehouden.
De functie hieronder blijft snel omdat die altijd getallen ontvangt; als je een tekenreeks doorgeeft, veroorzaakt dat een deoptimalisatie en een breder frame in het profiel.
function add(a, b) {
return a + b;
}
let total = 0;
for (let i = 0; i < 1e7; i++) {
total = add(total, i); // monomorphic: always numbers, stays optimized
}
console.log(total);Een herhaalbare profileringsworkflow
Breng de onderdelen samen in een lus die je op elke backend kunt uitvoeren:
- Reproduceer de belasting op een deterministische manier (met een benchmark of opnieuw afgespeeld verkeer), zodat twee profielen vergelijkbaar zijn.
- Registreer een profiel (
--prof, deinspector-sessie of0x). - Lees de flame graph: het breedste bladframe = de meeste eigen tijd = het eerste doel.
- Los precies die functie op en verander verder niets.
- Profileer opnieuw onder dezelfde belasting en controleer of de dikke balk smaller is geworden.
Verander per iteratie één ding. Als je drie functies tegelijk aanpast, weet je niet welke wijziging heeft geholpen — en een ervan kan de situatie hebben verslechterd.
Korte controle: de graph lezen
Je hebt een traag eindpunt geprofileerd. In de geaggregeerde flame graph is je aanvraagafhandelaar het breedste frame, maar erboven staan hoge stacks met onderliggende functies. Eén klein bladframe bovenaan, JSON.stringify, is ook erg breed. Welke functie moet je als eerste optimaliseren?
Samenvatting
Je kunt nu CPU-paden met hoge belasting in een Node.js-backend vinden en oplossen:
- Eigen tijd (exclusief) is waar de CPU wordt belast; totale tijd (inclusief) omvat onderliggende functies. Optimaliseer op basis van eigen tijd.
- Registreer met
node --prof, de ingebouwdeinspector-sessie of0xvoor een interactieve flame graph. - In een flame graph is de x-as het aantal monsters (CPU), niet de tijd, en de y-as de stackdiepte. Zoek het breedste bladframe.
- Brede
GC/Scavenge-frames betekenen druk door geheugentoewijzingen — wijs minder geheugen toe in plaats van logica op detailniveau te optimaliseren. - Gebruik
--trace-deoptom deoptimalisaties door polymorfe code die van vorm verandert op te sporen. - Werk in een lus: reproduceer, registreer, lees, los één ding op en profileer opnieuw.
Leer JavaScript met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 22
- Lessen
- 92
Veelgestelde vragen
Is de les “CPU-profiling en flamegraphs voor hot paths” gratis?
Ja — de volledige tekst van “CPU-profiling en flamegraphs voor hot paths” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Bootcamp backendontwikkeling met Node.js wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.
Wat leer ik in “CPU-profiling en flamegraphs voor hot paths”?
Leg CPU-profielen vast en lees flamegraphs om functies te vinden die de meeste tijd verbruiken. Je oefent met Bootcamp backendontwikkeling met Node.js door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Bootcamp backendontwikkeling met Node.js te beginnen?
Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met Node.js op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “CPU-profiling en flamegraphs voor hot paths”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met Node.js?
Ja. Elke les over Bootcamp backendontwikkeling met Node.js bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- De V8-heap, generational GC en levensduur van objecten
- Heapsnapshots vastleggen en vergelijken
- CPU-profiling en flamegraphs voor hot paths
- Veelvoorkomende lekpatronen detecteren en oplossen