NNODE LOOP LABruntime observatoryNNEON · Articles in 90 languages
CONNECTING
v24.18.0linux/x64
CPU-bound work
04

Blocking vs Worker

Compare a heartbeat gap caused by main-thread blocking with the same calculation in a Worker Thread.

PROCESS IDcurrent server
UPTIMEsince startup
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Timeline

READY
0 ms
Events will appear hereRun the selected scenario
#TIMESOURCEEVENT
Waiting for an experiment…
CHAPTER 04
DEEP DIVE · FROM BASICS TO CODE

Understanding: Blocking vs Worker

You can read this chapter before running the experiment. Then return to the live trace and match each concept to a real event.

FIRST, IN PLAIN LANGUAGE

The main thread is like the only emergency operator. If they spend minutes calculating a spreadsheet, they stop answering calls. A Worker is another operator that can perform the calculation while the first line stays available.

TECHNICAL FOUNDATION

The Event Loop scales waiting for I/O, not CPU-bound JavaScript. A Worker Thread creates a separate V8 isolate, call stack, and Event Loop inside the same process. Data is exchanged through structured cloning, transfer lists, or SharedArrayBuffer.

Why it mattersBlocking the main thread delays every route, timer, and client in the process. Workers preserve responsiveness but require a bounded pool and careful data transfer.
WHERE THE WORK RUNS
01YOUR JS CODEfunctions · callbacks
02NODE APIsfs · crypto · timers
03V8 + LIBUVheap · loop · pool
04OPERATING SYSTEMI/O · threads · memory
01 · GLOSSARY

Terms used in this experiment

Understand the words first, then the execution order.

01

Main Thread

The thread running Express, HTTP callbacks, and the primary application JavaScript.

02

CPU-bound

Work whose speed is limited by CPU calculation rather than waiting for an external resource.

03

Worker Thread

A separate Node JavaScript environment with its own V8 isolate.

04

Message passing

Data exchange through postMessage; values are cloned, transferred, or shared.

02 · MECHANICS

What happens step by step

Each step maps to an observable runtime state.

  1. 01
    Main heartbeat

    A timer creates control events roughly every 70 ms.

  2. 02
    Blocking

    A synchronous CPU loop does not return control to the Event Loop.

  3. 03
    Accumulated lag

    The heartbeat cannot interrupt the calculation and runs only afterward.

  4. 04
    Worker creation

    The same calculation idea runs in a separate V8 isolate.

  5. 05
    Result message

    Main keeps producing heartbeats and later receives an async message callback.

03 · CONTEXT

Where the result needs context

These details explain why similar code can sometimes produce a different trace.

01

Responsive does not mean faster

The experiment proves that the main thread stays available. It does not guarantee lower compute time: Worker creation, isolate startup, and data transfer all have a cost.

02

Threads still share CPU

The Worker is separate from the server Event Loop but competes for cores, memory, and the same process/container cgroup. Heartbeats may still jitter on a saturated CPU.

03

A Worker is not a child process

A Worker has its own V8 isolate and JavaScript heap but remains inside the same process and can use shared memory. Isolation is weaker than child_process, while communication is usually cheaper.

04

Large messages have a cost

Normal values use structured cloning. Large ArrayBuffers can transfer ownership through a transfer list; SharedArrayBuffer requires application-level synchronization.

01
Theory

First understand which parts of Node participate in execution.

02
Simplified code

Then remove instrumentation and focus on the central mechanism.

03
Runtime code

Finally match the model to the code that produces the live trace.

04 · Simplified code

A minimal model without instrumentation

src/demos.js · educational snippetJavaScript
// Bad on the main thread:
heavyCpuWork(360);

// Move CPU-bound work out:
const worker = new Worker('./cpu-worker.js');
worker.on('message', result => {
  console.log(result);
});
05 · Runtime code

The complete code executed by the scenario

This is not an alternative example: these are the functions and files used by the Run button.

ACTUAL SOURCE

The source is generated from the real server function. Scenarios using a child process or Worker include every participating file.

src/demos.js
scenario91 lines
import { performance } from 'node:perf_hooks';
import { Worker as ThreadWorker } from 'node:worker_threads';

const workerPath = new URL('./cpu-worker.js', import.meta.url);

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

function blockMainThread(durationMs) {
  const startedAt = performance.now();
  let iterations = 0;

  // Намеренная блокировка для учебного сценария. В production так делать нельзя.
  while (performance.now() - startedAt < durationMs) {
    iterations += Math.sqrt((iterations % 10_000) + 1);
  }

  return Math.round(iterations);
}

function startHeartbeat(emit, lane, label, durationMs) {
  const intervalMs = 70;
  const startedAt = performance.now();
  let expectedAt = startedAt + intervalMs;
  let number = 0;

  const interval = setInterval(() => {
    number += 1;
    const now = performance.now();
    const lag = Math.max(0, Math.round(now - expectedAt));
    emit(
      lane,
      lag > 80 ? 'warning' : 'heartbeat',
      `${label} #${number}; задержка ${lag} мс`,
    );
    expectedAt = now + intervalMs;
  }, intervalMs);

  return new Promise((resolve) => {
    setTimeout(() => {
      clearInterval(interval);
      resolve(Math.round(performance.now() - startedAt));
    }, durationMs);
  });
}

async function blockingComparison(emit) {
  emit('result', 'section', 'Часть A — CPU-работа в главном потоке');
  const mainHeartbeat = startHeartbeat(emit, 'main-thread', 'Пульс main', 650);

  await sleep(150);
  emit('call-stack', 'warning', 'Блокируем главный поток примерно на 360 мс');
  const iterations = blockMainThread(360);
  emit(
    'call-stack',
    'sync',
    `Главный поток снова свободен (${iterations.toLocaleString('ru-RU')} итераций)`,
  );
  await mainHeartbeat;

  emit('result', 'section', 'Часть B — та же работа в Worker Thread');
  const workerHeartbeat = startHeartbeat(emit, 'main-thread', 'Пульс main', 650);

  await sleep(150);
  emit('worker-thread', 'schedule', 'CPU-задача отправлена отдельному Worker');

  const workerResult = await new Promise((resolve, reject) => {
    const worker = new ThreadWorker(workerPath, {
      workerData: { durationMs: 360 },
    });

    worker.once('message', resolve);
    worker.once('error', reject);
    worker.once('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker завершился с кодом ${code}`));
    });
  });

  emit(
    'worker-thread',
    'callback',
    `Worker закончил за ${workerResult.durationMs} мс; main всё это время отправлял пульс`,
  );
  await workerHeartbeat;

  emit(
    'result',
    'result',
    'Сравните разрыв heartbeat в части A с равномерными событиями в части B',
  );
}

The emit(...) calls become live-trace rows. await and Promise keep the HTTP stream open until the scenario completes.

06 · DO NOT CONFUSE

Common misconceptions

The myth is on the left; the accurate model is on the right.

MYTH

Wrapping a calculation in async moves it to another thread.

ACTUALLY

async changes the return value to a Promise; synchronous body code still runs in the current thread.

MYTH

A Promise makes work parallel.

ACTUALLY

A Promise models a future value; the API decides where the work executes.

MYTH

Create a new Worker for every HTTP request.

ACTUALLY

Isolate startup is expensive; production systems usually use a bounded worker pool.

MYTH

A Worker is guaranteed to speed up every task.

ACTUALLY

A short task may become slower because of startup and messaging. The primary win in this lab is main-thread responsiveness.

07 · SELF-CHECK

Explain it in your own words

If you can explain the answer without quoting documentation, your mental model is starting to take shape.

  1. Why can setInterval not interrupt a while loop?
  2. Which values are expensive to send through structured cloning?