Skip to content

JavaScript in the browser

Short answer: In the browser, JavaScript is a single-threaded, event-driven runtime where the event loop schedules tasks and microtasks, and rendering happens only when the main thread is free.

Core mental model (browser JS)

  • JavaScript runs inside a renderer process, typically on a single “main thread”.
  • The same main thread is responsible for:
    • Running your JS
    • Handling user input events (dispatching them to listeners)
    • Coordinating style/layout/paint work (and calling into the compositor/GPU)
  • Result: long JS blocks everything (input lag + dropped frames + delayed rendering).

The event loop in one picture (conceptual)

  • There are multiple queues (not just one):
    • Task (a.k.a. macrotask) queue(s): timers, DOM events, postMessage, network callbacks, etc.
    • Microtask queue: Promise reactions (.then/.catch/.finally), queueMicrotask, MutationObserver
    • Render steps: style/layout/paint/composite (not a “queue”, but a phase the browser attempts between tasks)
  • The loop is: pick a task → run it → run ALL microtasks → (maybe) render → pick next task.

Tasks vs microtasks (what runs when)

Tasks (macrotasks) — “next turn”

Common sources:

  • setTimeout / setInterval callbacks
  • UI events (click, input, scroll) dispatch
  • message channel / postMessage
  • some network callbacks (browser-specific scheduling details exist)

Semantics:

  • Only one task runs at a time.
  • After the task finishes, the browser drains microtasks.
  • Rendering is typically attempted between tasks (after microtasks), if needed.

Microtasks — “end of this turn, before render”

Common sources:

  • Promise resolution handlers: promise.then(...)
  • async/await continuations (await essentially schedules Promise reactions)
  • queueMicrotask(...)
  • MutationObserver callbacks

Semantics:

  • Microtasks run after the current JS stack unwinds, before the browser renders.
  • Microtasks are drained completely (to exhaustion). If microtasks keep scheduling microtasks, rendering can starve.

Key pitfall:

  • “Promise.then is faster than setTimeout” is imprecise; it’s “Promise.then runs before the browser gets a chance to render and before the next task”.

Why rendering is tied to the event loop

  • The browser generally does not render “in the middle” of running JS.
  • Rendering steps need the main thread to:
    • Calculate style
    • Run layout
    • Produce paint records (or update them)
  • If JS keeps the main thread busy, frames drop.

Rule of thumb:

  • If you want the UI to update, you must yield back to the event loop and allow a render opportunity.

Frame budget and jank (practical performance model)

  • On a 60Hz display, a frame is ~16.67ms.
  • You don’t get the full 16ms for JS; style/layout/paint and other browser work also need time.
  • If a task runs for 50ms:
    • Input events are delayed (feels laggy)
    • Rendering cannot happen on time (stutters)
  • “Jank” is often “main thread work exceeds frame budget”.

Common scheduling tools and what they mean

setTimeout(fn, 0)

  • Schedules fn as a task in a future turn.
  • It yields to the browser: microtasks drain, then the browser may render, then your timeout runs later.
  • Not deterministic timing; it’s “not now”.

Use cases:

  • Break up long work into chunks.
  • Allow DOM updates / paint to occur before continuing.

Pitfall:

  • Timer clamping (minimum delay), especially in background tabs.
  • Not aligned with frames.

Promise.resolve().then(fn) / queueMicrotask(fn)

  • Schedules fn as a microtask.
  • Runs before the browser renders and before the next task.
  • Useful for “after current call stack” logic, but can starve rendering if overused.

Use cases:

  • Ensure something happens after current synchronous code.
  • Batch state updates before letting the browser render.

Pitfall:

  • Infinite microtask chains block rendering:
    • then(() => Promise.resolve().then(...)) in a loop can freeze the UI.

requestAnimationFrame(fn)

  • Schedules fn to run before the next repaint (frame).
  • Browser calls fn with a high-resolution timestamp.
  • Intended for visual updates (animations, measuring layout in sync with rendering).

Use cases:

  • Animate based on time deltas.
  • Coordinate DOM reads/writes around frames.

Pitfall:

  • If your rAF callback is heavy, you still drop frames.
  • Not guaranteed in background tabs (throttled/paused).

requestIdleCallback(fn)

  • Calls fn when the browser is “idle” (has spare time).
  • Not reliable for critical work; can be delayed significantly.
  • Great for low-priority tasks.

Use cases:

  • Precompute, prefetch, analytics, cache warming.

Pitfall:

  • Not supported uniformly in all environments; timing is unpredictable.

A precise example: ordering (task vs microtask vs render)

Consider:

  • In a click handler (task):
    • Update DOM text
    • Schedule a Promise.then (microtask)
    • Schedule a setTimeout (task)
  • What tends to happen:
    1. click handler runs (task)
    2. microtasks run (Promise.then)
    3. browser may render (show DOM change)
    4. setTimeout runs later (next task)

Important nuance:

  • The DOM mutation in step (1) does not necessarily paint until after microtasks drain.
  • If microtasks run long, paint is delayed.

async/await in the browser (how it maps to microtasks)

  • async functions return Promises.
  • await pauses the function and schedules the continuation as a Promise reaction (microtask) once awaited Promise resolves.
  • If you “await Promise.resolve()” you yield to microtasks, not to rendering necessarily (render happens after microtasks).

Pitfall:

  • “await” is not “let the browser render now”.
  • If you want to yield to rendering, consider awaiting a task boundary (e.g., setTimeout) or using rAF depending on intent.

DOM events and input responsiveness

  • Input events (click, keydown, input) are dispatched as tasks.
  • If the main thread is busy, the event waits in the queue.
  • If your handler does heavy work:
    • You block subsequent input
    • You block rendering
  • Strategy: do minimal work in handlers, schedule heavy work elsewhere (chunking).

Layout thrashing (JS + rendering interaction trap)

Layout thrashing is alternating DOM writes and reads that force repeated layout.

Mechanism:

  • Certain DOM reads require up-to-date layout (e.g., offsetHeight, getBoundingClientRect).
  • If you write to DOM (change class/style) then read layout, the browser may have to:
    • Flush pending style changes
    • Recompute layout immediately
  • Repeating this in a loop causes many forced layouts.

Pattern to avoid:

  • write → read → write → read in tight loops

Better pattern:

  • Batch reads first, then batch writes:
    • Read all needed layout data
    • Then apply all DOM mutations
  • Or do writes in rAF to align with frame boundary.

Microtasks and rendering starvation (real failure mode)

Example failure mode:

  • A function schedules a microtask that schedules another microtask, repeatedly.
  • Because the browser drains microtasks fully before rendering, rendering never happens.
  • Symptoms:
    • Tab looks frozen
    • CPU spikes
    • No repaint even though DOM changes occur

Mitigation:

  • Insert task boundaries:
    • setTimeout(fn, 0) occasionally
    • or postMessage / MessageChannel yields
  • Keep microtasks bounded and short.

Web Workers (what “multithreading” really means in browser JS)

  • Workers run JS off the main thread.
  • They cannot access the DOM directly.
  • Communication is via message passing (structured clone) and optionally SharedArrayBuffer (advanced).

Use cases:

  • CPU-heavy computation (parsing, compression, crypto, analytics)
  • Keep main thread responsive

Pitfalls:

  • Serialization cost (copying large data)
  • Need careful design for data flow
  • Not a free win unless work is substantial

Timers, throttling, and background tabs

  • Browsers throttle timers in background tabs to save power.
  • requestAnimationFrame may be paused or heavily throttled in background.
  • setTimeout minimum delay is increased in some conditions.
  • Do not rely on precise timer behavior for correctness.

Correctness principle:

  • Timers are hints, not guarantees.
  • Use server time or monotonic clocks (performance.now) for measuring elapsed time, and design for delays.

Fetch and promises: where network fits

  • fetch(...) returns a Promise.
  • “Response arrived” leads to Promise resolution handlers (microtasks) running when the main thread is free.
  • Even if network completes quickly, JS can’t observe it until it gets scheduled.
  • This is why heavy JS can make “fast API” feel slow.

Practical patterns for backend engineers

Pattern: keep handlers light

  • Do minimal parsing/validation in event handlers.
  • Defer heavy work via tasks or workers.

Pattern: chunk long work

  • For loops over large arrays: process in slices, yielding between slices.
  • This preserves responsiveness and allows rendering.

Pattern: separate DOM reads and writes

  • Reads (measure) first, writes (mutate) after, ideally once per frame.

Pattern: choose the right scheduler

  • Need “after current stack, before render”: microtask (queueMicrotask / Promise.then)
  • Need “let render/input happen”: task boundary (setTimeout / MessageChannel)
  • Need “align with repaint”: requestAnimationFrame
  • Need “whenever spare time”: requestIdleCallback

Interview traps (high-signal)

  • “JavaScript is single-threaded”: true for main thread execution, but the platform is multithreaded (network, compositor, workers).
  • “await yields to event loop”: it yields to microtasks; rendering still waits until microtasks drain.
  • “Promise.then is asynchronous”: yes, but it is prioritized before rendering and before next task.
  • “DOM updates are immediate”: DOM mutations are immediate in memory, but painting is deferred.

Quick reference table

MechanismQueue/PhaseWhen it runsBest forCommon pitfall
setTimeout(fn, 0) TaskFuture turnYielding to render/input, chunkingTiming not guaranteed, throttling
Promise.then / queueMicrotaskMicrotaskEnd of current turn, before renderPost-stack logic, batchingStarves rendering if chained
requestAnimationFrameRender-alignedBefore next repaintAnimations, visual updatesHeavy callback drops frames
requestIdleCallbackIdleWhen browser is idleLow-priority workCan be delayed a lot
Web WorkerSeparate threadParallel to main threadCPU-heavy workSerialization overhead, no DOM