The JavaScript event loop that drives your web page behaves very differently from the one that powers a Node.js server, and the mismatch can freeze a UI or choke I/O if you’re not careful. Knowing where the two diverge is essential for anyone who writes async code that runs in both environments.

Why the distinction matters

The event loop isn’t defined by the ECMAScript spec; it lives in the host. Browsers must keep a page responsive while rendering frames, whereas Node.js is built around non-blocking I/O. Mixing patterns that work in one host with the other can produce bugs that are hard to reproduce: a long chain of promises may stall a browser’s repaint, while an unchecked process.nextTick loop can prevent Node from ever reaching its I/O phases.

The browser’s turn-based loop

In a browser, the loop runs a single cycle that interleaves task execution, microtask draining, and rendering:

  1. Run one macrotask (a click handler, a setTimeout, etc.).
  2. Drain all microtasks (promises, queueMicrotask).
  3. If a frame is due, paint and composite to hit the target 60 fps.
  4. Repeat.

Two APIs give developers explicit hooks into this cycle:

  • requestAnimationFrame – called just before the browser paints. It’s the right place for animation work because the callback runs after the current microtasks but before the next frame.
  • requestIdleCallback – invoked when the browser has no high-priority work. It’s useful for low-impact tasks such as analytics or pre-loading data.

Pitfall: microtask starvation

Because the browser empties the microtask queue before it renders, a long chain of promises can keep the UI from ever painting. The call stack isn’t blocked; the page simply never reaches the render step, which feels like a freeze to the user.

Node’s libuv-driven loop

Node.js delegates its loop to libuv, a C library that splits work into distinct phases, each with its own queue:

  1. Timers – callbacks from setTimeout and setInterval.
  2. Pending callbacks – deferred I/O callbacks that have already completed at the OS level.
  3. Poll – fetches new I/O events (file reads, network data).
  4. Check – runs setImmediate callbacks.
  5. Close callbacks – fires when a socket or handle closes.

Two constructs sit outside this phase order:

  • process.nextTick – runs before the microtask queue, immediately after the current operation finishes.

Pitfall: I/O starvation

If a function repeatedly schedules process.nextTick without yielding, Node never advances beyond the “next-tick” step. Network requests, file reads, and timers sit idle, causing server-side latency spikes or outright hangs.

setImmediate vs. setTimeout in practice

Both schedule callbacks for the next iteration, but their relative order depends on where they’re called:

  • Top-level code – the order isn’t guaranteed; it hinges on how quickly the process starts up.
  • Inside an I/O callback – the order is deterministic: setImmediate runs before setTimeout(fn, 0). After the Poll phase finishes, libuv moves to the Check phase (where setImmediate lives) before it would re-enter the Timers phase for a zero-delay timeout.

This subtlety matters when you rely on precise sequencing, such as cleaning up a resource right after a read completes.

Key differences at a glance

  • Goal: Browsers prioritize visual updates; Node prioritizes I/O readiness.
  • Rendering hook: requestAnimationFrame (browser only).
  • Phase-specific hook: setImmediate (Node only, fires in the Check phase).
  • High-priority queue: process.nextTick (Node only, runs before microtasks).
  • Starvation risk: Long promise chains in browsers; unbounded process.nextTick in Node.

What to watch next

If you maintain a codebase that runs in both environments (e.g., isomorphic libraries), audit any place where you:

  • Chain many promises without yielding to the event loop. Insert await new Promise(r => setTimeout(r, 0)) or use requestIdleCallback in the browser to give the renderer a chance.
  • Use process.nextTick for work that could be deferred. Prefer setImmediate or a regular promise when you don’t need “next-tick” urgency.
  • Assume setTimeout(fn, 0) and setImmediate are interchangeable. Test the ordering inside I/O callbacks if the sequence matters.

In sintesi

L'event loop è uno scheduler specifico dell'host, non una caratteristica universale di JavaScript. I browser integrano il rendering nel loop; Node isola l'I/O in fasi libuv. L'uso improprio dei meccanismi di priorità — le microtask nel browser, process.nextTick in Node — può privare di risorse la parte di sistema che ogni ambiente è progettato per servire. Allinea i tuoi pattern asincroni al modello di loop dell'host e eviterai sia il blocco delle pagine che quello dei server.