Waarom de event loop stilvalt bij CPU-intensief werk
Identificeer blokkerende berekeningen en begrijp waarom single-threaded JavaScript echte threads nodig heeft voor parallellisme.
Waarom de event loop stilvalt bij CPU-intensief werk is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 1 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.
Eén thread om alles uit te voeren
Node.js voert je JavaScript uit op één thread. Elke verzoekhandler, timercallback en voortzetting van een promise voert om de beurt uit op die ene thread, gecoördineerd door de gebeurtenislus.
Dit ontwerp is uitstekend voor I/O-gebonden werk: terwijl je wacht op een databasequery of HTTP-antwoord, kan de thread andere verzoeken afhandelen. Het wachten gebeurt elders (in het besturingssysteem en de threadpool van libuv), niet in je JavaScript.
Maar er is een addertje onder het gras. Als één callback besluit zware berekeningen uit te voeren, is die ene thread druk met rekenen en kan er niets anders worden uitgevoerd totdat dit klaar is. In deze les onderzoeken we precies waarom CPU-gebonden werk de gebeurtenislus blokkeert.
De gebeurtenislus in één afbeelding
De gebeurtenislus is een eenvoudig idee: een lus die steeds de volgende gereedstaande callback uit een wachtrij haalt en deze volledig uitvoert voordat hij de volgende ophaalt.
- Een timer gaat af → voer de callback uit.
- Een socket heeft gegevens → voer de callback uit.
- Een promise wordt vervuld → voer de voortzetting uit.
Belangrijk is dat callbacks coöperatief zijn: de lus kan een callback niet halverwege onderbreken. JavaScript kent geen preëmptie. Elke callback moet vrijwillig terugkeren om de lus de kans te geven iets anders te doen.
const server = require('http').createServer((req, res) => {
// This callback must RETURN quickly so the loop can serve
// the next request. The loop won't interrupt it midway.
res.end('ok');
});
server.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});I/O-gebonden versus CPU-gebonden
Om te begrijpen waardoor de uitvoering stagneert, begin je met het classificeren van het werk:
- I/O-gebonden: de meeste tijd wordt besteed aan het wachten op een externe bron — schijf, netwerk of database. De CPU is grotendeels inactief.
- CPU-gebonden: de meeste tijd wordt besteed aan berekenen — hashes berekenen, afbeeldingen schalen, enorme JSON-gegevens parsen, comprimeren, cryptografie of grote lussen. De CPU draait op 100%.
Node's model met één thread blinkt uit bij I/O-gebonden werklasten, omdat wachten de thread niet bezet. Het heeft moeite met CPU-gebonden werklasten, omdat berekeningen de thread wel bezetten — en er maar één is.
Asynchrone I/O blokkeert niet
Asynchrone I/O is niet-blokkerend, juist omdat het langdurige wachten wordt uitbesteed. Wanneer je een asynchrone bestandslezing aanroept, geeft Node het werk aan libuv (en het besturingssysteem), registreert een callback en geeft onmiddellijk de controle terug aan de gebeurtenislus.
De lus kan duizenden andere dingen afhandelen terwijl de schijf zijn werk doet. Wanneer het lezen klaar is, wordt je callback in de wachtrij geplaatst. De thread heeft nooit doelloos zitten wachten.
Dit is het belangrijkste inzicht: wachten op I/O kost de gebeurtenislus niets. De thread wordt vrijgegeven om ander werk te doen.
const fs = require('fs/promises');
async function main() {
console.log('before read');
// Control returns to the loop while the disk works.
const data = await fs.readFile(__filename, 'utf8');
console.log('read', data.length, 'bytes');
}
main();
console.log('this prints BEFORE the read finishes');Een CPU-gebonden functie die blokkeert
Vergelijk dat nu met berekeningen. Een strakke lus die een miljard getallen optelt, wacht nergens op — de thread blijft van begin tot eind volledig bezet.
Terwijl heavyCompute() draait, is de gebeurtenislus bevroren. Er gaan geen timers af, binnenkomende verzoeken worden niet geaccepteerd en voortzettingen van promises worden niet uitgevoerd. Het hele proces lijkt vastgelopen totdat de functie terugkeert.
Voer dit uit en merk op dat het programma pas uitvoer produceert wanneer de lus klaar is — tijdens de berekening is er geen moment waarop de controle terugkeert naar de lus.
function heavyCompute() {
let total = 0;
for (let i = 0; i < 2_000_000_000; i++) {
total += i;
}
return total;
}
console.time('compute');
const result = heavyCompute();
console.timeEnd('compute');
console.log('result =', result);Bewijs: timers raken uitgehongerd tijdens berekeningen
Hier ziet u rechtstreeks hoe de blokkering ontstaat. We plannen een timer in voor 100 ms en starten daarna onmiddellijk een CPU-intensieve lus die veel langer duurt.
U verwacht misschien dat de timer na 100 ms afgaat. Dat gebeurt niet. De lus is druk aan het rekenen en kan zichzelf niet onderbreken om de callback van de timer uit te voeren. De timer gaat pas af nadat de berekening is voltooid — vaak seconden te laat.
Dit is de blokkering van de gebeurtenislus in zijn zuiverste vorm: een callback die in de wachtrij staat en klaar is om te worden uitgevoerd, maar geen toegang krijgt tot de thread.
const start = Date.now();
setTimeout(() => {
console.log('Timer fired after', Date.now() - start, 'ms (asked for 100)');
}, 100);
// Block the single thread for ~2 seconds.
const end = Date.now() + 2000;
while (Date.now() < end) {
// busy-wait, no I/O, no yielding
}
console.log('Blocking loop done after', Date.now() - start, 'ms');async/await helpt niet bij CPU-werk
Een veelvoorkomende misvatting: CPU-werk in een async-functie plaatsen of await toevoegen zou het niet-blokkerend maken. Dat is niet zo.
async/await geeft de thread alleen vrij bij een echt await-punt dat wacht op een echte asynchrone bewerking (I/O, een timer, een microtask). Een pure berekening heeft zo'n onderbrekingspunt niet — die wordt synchroon uitgevoerd, ongeacht het sleutelwoord async.
In het voorbeeld verandert het markeren van de functie als async niets: de lus blokkeert nog steeds gedurende de volledige looptijd.
async function compute() {
let total = 0;
// No await inside a hot loop = still fully synchronous.
for (let i = 0; i < 2_000_000_000; i++) {
total += i;
}
return total;
}
setTimeout(() => console.log('timer wanted at 50ms'), 50);
console.time('compute');
compute().then((r) => {
console.timeEnd('compute');
console.log('result =', r);
});Waarom dit een backend lamlegt
Op een server betekent één geblokkeerde thread dat elke gelijktijdige client hier last van heeft. Terwijl één aanvraag een CPU-taak van 3 seconden uitvoert, wachten alle andere aanvragen die op die thread in de wachtrij staan de volledige 3 seconden voordat ze überhaupt worden gelezen.
- De latentie schiet omhoog voor niet-gerelateerde eindpunten.
- Healthcheck-pings lopen vast door een time-out en orkestrators kunnen het "niet-reagerende" proces beëindigen.
- De verwerkingscapaciteit stort in: één core, één taak tegelijk.
De server is niet gecrasht — hij wordt eenvoudigweg gem monopoliseerd. Daarom kan één zware synchrone handler onder belasting een volledige Node-service platleggen.
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/heavy') {
let t = 0;
for (let i = 0; i < 5_000_000_000; i++) t += i; // blocks everyone
return res.end('done ' + t);
}
// /ping cannot respond while /heavy is running on the same thread
res.end('pong');
}).listen(3000);Opsplitsen helpt een beetje, maar niet genoeg
Een gedeeltelijke oplossing is om het werk in stukken op te delen en tussen die stukken setImmediate te gebruiken, zodat de lus tussen de delen door even ruimte krijgt.
Hierdoor blijft de server responsief — pings kunnen tussen de stukken door worden beantwoord — maar dit zorgt niet voor parallellisme. De berekening wordt nog steeds op één thread uitgevoerd, nu afgewisseld met andere callbacks. Daardoor duurt de totale kloktijd voor de zware taak meestal langer in plaats van korter.
Opsplitsen ruilt latentie voor anderen in tegen verwerkingscapaciteit voor de taak. Het kan geen tweede CPU-core gebruiken. Voor echt parallellisme hebt u echte threads nodig.
function computeChunked(total, i, end, done) {
const sliceEnd = Math.min(i + 10_000_000, end);
for (; i < sliceEnd; i++) total += i;
if (i < end) {
// Yield to the loop, then continue next tick.
setImmediate(() => computeChunked(total, i, end, done));
} else {
done(total);
}
}
computeChunked(0, 0, 2_000_000_000, (r) => console.log('result =', r));
setInterval(() => console.log('loop still alive'), 200);De echte oplossing: verplaats het werk van de hoofdthread
Omdat JavaScript op de hoofdthread single-threaded en coöperatief is, kunt u CPU-intensief werk alleen parallel uitvoeren — met meer dan één CPU-core — door het naar een afzonderlijke uitvoerthread te verplaatsen.
Node biedt hiervoor verschillende mogelijkheden:
- Worker Threads (
worker_threads): echte threads van het besturingssysteem binnen hetzelfde proces, elk met een eigen gebeurtenislus en V8-isolate. Ideaal voor CPU-intensieve taken. - Cluster / child processes: afzonderlijke processen, met meer isolatie maar ook meer overhead.
De hoofdthread draagt de zware taak uit, blijft responsief voor I/O en ontvangt het resultaat via een bericht wanneer de worker klaar is.
Een workerthread houdt de lus vrij
Dit is de vorm van de oplossing. De hoofdthread start een Worker die de zware berekening op een andere thread en een andere core uitvoert. De hoofdgebeurtenislus blijft vrij om aanvragen te verwerken en op timers te reageren.
Wanneer de worker klaar is, stuurt die een bericht terug. De callback van de hoofdthread verwerkt dat resultaat via de gebeurtenislus — niet-blokkerend, parallel en schaalbaar over meerdere cores.
Dit vormt de basis voor CPU-intensief parallellisme in Node, waarop de rest van deze cursus voortbouwt.
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (sum) => console.log('worker result =', sum));
// Main loop is NOT blocked: this timer fires on time.
setInterval(() => console.log('main thread responsive'), 200);
} else {
let total = 0;
for (let i = 0; i < 2_000_000_000; i++) total += i;
parentPort.postMessage(total);
}Korte controle
Een Node HTTP-handler voert een synchrone lus van 4 seconden uit om een afbeelding te hashen. Wat gebeurt er gedurende die 4 seconden met een tweede client die een ander, lichtgewicht eindpunt aanroept?
Samenvatting
De belangrijkste punten uit deze les:
- Node voert JavaScript uit op één coöperatieve thread van de gebeurtenislus — callbacks worden volledig uitgevoerd en kunnen niet worden onderbroken.
- I/O-gebonden werk is niet-blokkerend omdat het wachten wordt gedelegeerd aan libuv/het besturingssysteem; de thread wordt vrijgegeven.
- CPU-gebonden werk bezet de thread gedurende de volledige uitvoering en bevriest timers, aanvragen en vervolgingen van promises — de gebeurtenislus blokkeert.
async/awaithelpt niet bij pure berekeningen; het geeft de thread alleen vrij bij echte asynchrone onderbrekingspunten.- Opsplitsen met
setImmediateherstelt de responsiviteit, maar voegt geen parallellisme toe en kan geen extra cores gebruiken. - De echte oplossing is Worker Threads: verplaats CPU-gebonden werk naar een afzonderlijke thread/core, zodat de hoofdlus vrij blijft.
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 “Waarom de event loop stilvalt bij CPU-intensief werk” gratis?
Ja — de volledige tekst van “Waarom de event loop stilvalt bij CPU-intensief werk” 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 “Waarom de event loop stilvalt bij CPU-intensief werk”?
Identificeer blokkerende berekeningen en begrijp waarom single-threaded JavaScript echte threads nodig heeft voor parallellisme. 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 1 van 4.
Hoe lang duurt de les “Waarom de event loop stilvalt bij CPU-intensief werk”?
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
- Waarom de event loop stilvalt bij CPU-intensief werk
- Worker threads starten en berichten doorgeven
- Geheugen delen met SharedArrayBuffer en Atomics
- Een herbruikbare workerpool bouwen voor throughput