Performance tuning Node.js draait niet om magische trucjes, maar om meten, begrijpen en gericht optimaliseren. Een snelle app voelt professioneel, schaalt beter en bespaart je flink wat serverkosten.
In deze gids loop je stap voor stap door de belangrijkste technieken: van event loop monitoring tot clustering, caching, profiling en productietips. Je krijgt concrete code en tools die je vandaag nog kunt toepassen.
Een kleine waarschuwing vooraf: optimaliseer nooit zonder te meten. Gut feeling leidt vaker tot verspilde tijd dan tot echte winst.
Stap 1: Begrijp waar je bottleneck zit
Voordat je code aanpast, moet je weten waar de tijd naartoe gaat. Node.js is single-threaded voor JavaScript-uitvoering, dus alles draait om de event loop.
Als je nog niet vertrouwd bent met hoe dit werkt, lees dan eerst wat Node.js is en hoe de event loop werkt. Dat geeft je de basis om de rest van dit artikel goed te snappen.
De meest voorkomende bottlenecks zijn:
- CPU-bound: zware berekeningen die de event loop blokkeren
- I/O-bound: trage databases of externe APIs
- Memory: leaks of te grote objecten in het geheugen
- Netwerk: latency, payload-grootte of te veel round-trips
Elke bottleneck heeft een andere oplossing, dus diagnose is stap één.
Stap 2: Monitor de event loop
De event loop is het hart van je applicatie. Als die vertraagt, vertraagt alles.
Gebruik perf_hooks om event loop lag te meten:
import { monitorEventLoopDelay } from 'node:perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log('p99 lag (ms):', histogram.percentile(99) / 1e6);
histogram.reset();
}, 5000);
Waarden boven de 100ms duiden op serieuze problemen. Zoek dan naar synchrone code zoals JSON.parse op grote objecten, bcrypt zonder async variant, of loops over grote arrays.
Verplaats zware berekeningen naar background jobs of worker threads. Zo blijft je hoofdthread responsief voor HTTP-requests.
Stap 3: Profiling met de juiste tools
Gokken is verliezen. Profileer voordat je optimaliseert.
Clinic.js
Clinic.js is een suite van tools die je bottlenecks visueel laat zien:
npx clinic doctor -- node server.js
npx clinic flame -- node server.js
npx clinic bubbleprof -- node server.js
- Doctor detecteert automatisch of je een event loop, I/O of garbage collection probleem hebt
- Flame toont een flamegraph van je CPU-tijd
- Bubbleprof visualiseert async operations
Ingebouwde profiler
Voor snelle CPU-analyse werkt de built-in profiler prima:
node --prof server.js
# na belasting:
node --prof-process isolate-*.log > profile.txt
Chrome DevTools
Start je app met --inspect en open chrome://inspect. Je kunt heap snapshots maken, CPU-profielen draaien en direct door je code steppen, net als in de browser.
Stap 4: Optimaliseer async code
Slechte async patronen zijn een van de grootste prestatiekillers. Als je twijfelt over promises en async/await, lees dan de gids over async programming in Node.js.
Een klassieke fout: sequentiële awaits waar parallel kan.
// Traag: 3x wachten
const user = await getUser(id);
const orders = await getOrders(id);
const prefs = await getPrefs(id);
// Snel: parallel
const [user, orders, prefs] = await Promise.all([
getUser(id),
getOrders(id),
getPrefs(id),
]);
Pas op met Promise.all als één van de calls faalt en je partial resultaten wilt: gebruik dan Promise.allSettled.
Vermijd ook for await ... of bij onafhankelijke iteraties, die draait sequentieel. Batch of parallelliseer waar mogelijk.
Stap 5: Caching slim inzetten
Niets is sneller dan data die je niet hoeft op te halen. Caching is vaak de grootste snelheidsboost die je kunt toevoegen.
In-memory cache
Voor kleine, vaak geraadpleegde data werkt een simpele LRU cache uitstekend:
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 5, // 5 minuten
});
async function getUser(id) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.users.findById(id);
cache.set(id, user);
return user;
}
Redis voor gedeelde cache
Draai je meerdere instances? Dan heb je een gedeelde cache nodig. Redis is de de facto standaard voor session stores, rate limiting en query caching.
Pas op voor cache-invalidatie, "There are only two hard things in Computer Science: cache invalidation and naming things." Gebruik korte TTLs en event-based invalidation waar mogelijk.
Stap 6: Database queries tunen
Bij veel apps is de database de echte bottleneck, niet Node zelf. Lees de gids over werken met databases in Node.js voor de basis.
Quick wins:
- Indexen: draai
EXPLAINop trage queries en voeg indexen toe waar nodig - Connection pooling: open geen nieuwe connectie per request
- N+1 queries: gebruik joins of batch-loaders in plaats van queries in loops
- Select only what you need:
SELECT *is zelden de juiste keuze
Een praktisch voorbeeld van een N+1 fix:
// Slecht: N+1
const posts = await db.posts.findAll();
for (const post of posts) {
post.author = await db.users.findById(post.authorId);
}
// Goed: één query met join of IN-clause
const posts = await db.posts.findAll({
include: [{ model: db.users, as: 'author' }],
});
Stap 7: Clustering en worker threads
Node draait standaard op één CPU-core. Op een 8-core server laat je dus 87% van je capaciteit liggen.
Cluster module
De cluster module (of beter: een process manager zoals PM2) start meerdere Node-processen die requests delen:
import cluster from 'node:cluster';
import { cpus } from 'node:os';
if (cluster.isPrimary) {
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
} else {
await import('./server.js');
}
In productie draai je meestal achter een reverse proxy zoals Nginx, of je gebruikt Kubernetes om meerdere containers te schalen.
Worker threads
Voor CPU-intensieve taken binnen één proces, denk aan image processing of zware encryptie, gebruik je worker threads. Die draaien echt parallel en blokkeren je event loop niet.
Stap 8: HTTP en payload optimalisaties
Kleine aanpassingen op HTTP-niveau geven vaak grote winst.
- Compressie: gebruik gzip of brotli (
compressionmiddleware in Express) - Keep-alive: zorg dat je HTTP agent connecties hergebruikt
- HTTP/2 of HTTP/3: via Nginx of een CDN voor multiplexing
- Kleinere responses: stuur geen velden die de client niet nodig heeft
- Streaming: gebruik streams voor grote files in plaats van alles in memory te laden
Voor REST API design best practices hebben we een aparte gids, pagination en field filtering zijn daar ook aan bod gekomen.
Stap 9: Memory management
Memory leaks zijn stille killers. Je app wordt geleidelijk trager totdat de server crasht.
Typische oorzaken:
- Globale arrays of Maps die blijven groeien
- Event listeners die nooit worden opgeruimd
- Caches zonder TTL of maxgrootte
- Closures die grote objecten vasthouden
Draai heap snapshots via Chrome DevTools en vergelijk er twee: één na warm-up, één na een uurtje load. Objecten die blijven groeien zijn verdacht.
Stel ook een realistische --max-old-space-size in. Standaard is dit ongeveer 1.5GB, wat voor grote apps te weinig kan zijn:
node --max-old-space-size=4096 server.js
Stap 10: Observability in productie
Profileren op je laptop is niet genoeg. Productie gedraagt zich anders dan je ontwikkelomgeving.
Gebruik een APM-tool zoals OpenTelemetry, New Relic of Datadog. Die geven je:
- Request latency per endpoint (p50, p95, p99)
- Event loop lag over tijd
- Memory en CPU trends
- Distributed tracing over services heen
Combineer dit met goede error handling op schaal en je ziet problemen voordat je gebruikers klagen.
Checklist: performance tuning in het kort
- Meet eerst, optimaliseer daarna
- Houd event loop lag onder 50ms
- Profileer met Clinic.js of Chrome DevTools
- Parallelliseer onafhankelijke async calls
- Cache agressief, invalideer zorgvuldig
- Fix N+1 queries en voeg database indexen toe
- Gebruik clustering op multi-core servers
- Verplaats CPU-werk naar worker threads
- Compressie, keep-alive en HTTP/2 aanzetten
- Monitor memory en stel heap limits in
- Zet APM aan in productie
Veelgestelde vragen
Wat is de grootste oorzaak van trage Node.js apps?
Meestal is het blokkeren van de event loop door synchrone code, zware berekeningen of onnodige database queries. Profileer eerst voor je optimaliseert, zodat je de echte bottleneck aanpakt en niet gokt.
Moet ik altijd clustering gebruiken in Node.js?
Niet altijd. Clustering helpt om meerdere CPU-cores te benutten, maar voegt complexiteit toe rond state en sessies. Voor CPU-intensieve workloads of hoge request volumes is het vrijwel altijd de moeite waard.
Welke tools gebruik ik het best voor Node.js profiling?
De ingebouwde --prof flag, Clinic.js, 0x en Chrome DevTools zijn uitstekende opties. Voor productie gebruik je APM-tools zoals New Relic, Datadog of OpenTelemetry voor continue observability.
Hoe voorkom ik memory leaks in Node.js?
Let op globale variabelen, ongebruikte event listeners en caches zonder limiet. Gebruik heap snapshots om lekken op te sporen en stel altijd een TTL in op in-memory caches zodat ze niet oneindig groeien.
Verbetert HTTP/2 de performance van mijn Node.js app?
Ja, vooral bij veel kleine requests dankzij multiplexing en header compressie. Gebruik een reverse proxy zoals Nginx of een CDN om HTTP/2 (of HTTP/3) af te handelen, zodat je Node-app zich op logica kan focussen.
Afsluitend
Performance tuning is geen eenmalige exercitie maar een continu proces. Bouw monitoring in vanaf dag één, stel duidelijke latency-budgetten vast en optimaliseer op basis van echte data.
Begin klein: meet je event loop, fix je traagste queries en zet compressie aan. Die drie stappen alleen al leveren vaak 80% van de winst op. De rest is finetunen, en dat doe je het best met een goed profiel in de hand.