Event LoopGENERALENGINE-SPECIFICDEVICE-SPECIFIC

What the Main Thread Owns

Script, DOM, style, layout, event dispatch and the accessibility tree share one thread — and knowing what is not on it is what makes moving work possible.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Which work must happen on the thread that owns the DOM, and which work is already somewhere else?

The user intent

A person wants an interface that keeps responding while it is doing something. Whether that is possible depends entirely on whether the work needs the one thread that can touch the document.

The obvious build

The browser is single-threaded, so everything is on the main thread and the only lever is to make code faster.

Why it breaks

The browser is emphatically not single-threaded. Networking, image decoding, rasterisation, compositing and often some animation run elsewhere; the *page's JavaScript and DOM* are what is confined to one thread (The Multi-Process Browser).

How it breaks in a real browser
  • The browser is emphatically not single-threaded. Networking, image decoding, rasterisation, compositing and often some animation run elsewhere; the *page's JavaScript and DOM* are what is confined to one thread (The Multi-Process Browser).
  • That distinction is the whole reason a transform animation keeps running smoothly while the thread is blocked, and a top animation does not (Cheap and Expensive Animation).
  • It is also why "move it to a worker" is sometimes a complete fix and sometimes impossible: a worker has no DOM, so anything that must read or write the document cannot go there (Web Workers and the DOM Boundary).
  • Believing everything is on one thread leads to optimising the wrong things — compressing an image to help a scroll that was actually blocked by style recalculation (Style Invalidation).
  • It also hides the cheapest wins. A great deal of what occupies the main thread is not your application logic at all: it is parsing, script evaluation, style, layout and garbage collection (The Real Cost of JavaScript).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • One thread per page — often shared by same-origin documents that can reach each other — runs your JavaScript, owns the DOM and CSSOM, computes style and layout, records paint operations, dispatches events and maintains the accessibility tree (The Multi-Process Browser).
  • The DOM is not thread-safe and was never designed to be. Confining it to one thread is what removes the need for locking in every line of frontend code, and the price is that everything DOM-adjacent contends for that thread (The DOM Is Not Your HTML).
  • Compositing runs on its own thread. Once layers exist, moving and fading them can be done without the main thread at all, which is why some animations survive a freeze (Compositing Layers).
  • Networking, TLS, decompression and much image and media decoding happen off the main thread. What lands back on it is the callback and any parsing your code does of the result (The Life of a Fetch).
  • Workers get their own thread and their own global scope with no DOM. Communication is by message, and by default by copy, so the boundary has a real cost that has to be weighed against the work moved (Structured Clone and Transferables).
  • Garbage collection largely runs on the main thread, interleaved with your tasks. Engines do substantial work incrementally and concurrently, but a collection that lands in a rendering phase is a dropped frame that appears in no application code (Memory Leaks).

What this makes the browser do

And which of it is avoidable.

  • Parsing HTML into a tree and CSS into the CSSOM, then computing a final value for every property on every element (Style Calculation).
  • Computing geometry for everything that could have moved, recording paint commands, and handing layers to the compositor (The Rendering Pipeline).
  • Hit-testing pointer positions, building event propagation paths, running listeners and applying default actions (How an Event Is Dispatched).
  • Deriving and maintaining the accessibility tree, and notifying platform accessibility APIs when it changes (The Accessibility Tree).
  • Executing your JavaScript, plus the engine's own parse, compile, optimise and collect work underneath it.

One thread for the document, many for everything else

The useful reframe is not "the browser is single-threaded" but "the DOM is single-threaded, and a lot of expensive work does not touch the DOM". Everything you can move is on the second side of that line.

The diagram separates the two. Note how much of the load path — fetching, decompressing, decoding — is already off the main thread, and how the work lands back on it precisely at the moment it becomes a document.

Where the work actually runs
callback + your parsingdecoded bitmappostMessage (copy or transfer)postMessagesteals timepaint commandstilesa11y notificationsNetwork + TLS + decompressionImage / media decodeGarbage collection (largely main thread)MAIN THREAD: script, DOM, style, layout, events, a11y treeWeb worker (no DOM)Raster threadsCompositor threadPixels + platform accessibility APIs
UserLLMAgentToolDataDecisionHumanGuardrail

What is on the thread, and can it move

This is the table to consult before proposing an optimisation. The third column is the one that decides whether an idea is viable at all; the fourth is what it will cost you if it is.

The recurring pattern is that anything requiring the document is stuck, and anything that is pure computation over data is movable. Most real work is a mixture, and the engineering is in finding the seam.

WorkRuns onCan it move off the main thread?What that costs
Your application logicMain threadYes, if it does not touch the DOMMessage boundary and serialisation (Talking to a Worker)
DOM read/writeMain threadNo — workers have no DOMKeep the DOM-touching slice small
Style calculation and layoutMain thread (mostly)No, but it can be reducedContainment, fewer nodes, simpler selectors (CSS Containment)
Event dispatch and hit testingMain threadNoKeep handlers short; make scroll handlers passive (Passive Listeners)
Accessibility-tree maintenanceMain threadNoEvery main-thread saving is also an accessibility saving
JSON / text parsing of a responseMain thread by defaultYes — parse in a workerThe parsed result must cross the boundary (Structured Clone and Transferables)
Network transfer and decompressionOther threads alreadyAlready offNothing to do; the callback is what costs you
Image decodeOften off-threadUsually already offLarge images still cost memory and raster time (Images and Fonts)
Raster and compositeCompositor and raster threadsAlready offLayer count and memory (Layer Explosion)
Garbage collectionLargely main threadNoAllocate less per frame; avoid churn in hot loops

Deciding where a piece of work belongs

The decision is usually made once per feature and is hard to reverse, so it is worth making explicitly. The criteria are the answers to three questions: does it need the DOM, does it need to be consistent with the frame being produced, and is it large enough that the boundary cost is worth paying.

A worker is not a general answer. For work that is small, that touches the document, or that must be coherent with the current frame, moving it is worse than leaving it — and the mitigation then belongs in the previous lesson's list instead (Long Tasks).

Where should this run?

Does it need the DOM, and is it big enough to be worth moving?

Main thread, inline

when Small, DOM-touching, or must be consistent with the frame about to be produced.

cost Every unit of time here is a unit the user might feel (Long Tasks).

Main thread, chunked across tasks

when DOM-touching and large: rendering a long list, mutating many nodes (Yielding and Scheduling).

cost Longer total time and visible intermediate states.

Worker

when CPU-bound, DOM-free, and large enough that the transfer cost is a small fraction: parsing, diffing, transforming, compressing (When a Worker Is Actually the Answer).

cost Serialisation, duplicated memory, and an asynchronous seam through previously synchronous code.

Compositor

when Continuous visual change that can be expressed as transform or opacity (Cheap and Expensive Animation).

cost Restricted to what the compositor can express; layers consume memory (Compositing Layers).

The server

when The result is shared across users or the client would be recomputing what the server already knows (Server-Side Rendering).

cost Server cost, cache invalidation, and a payload that may be larger than the input (How API Shape Drives UI Complexity).

How to build it

Most important first.

  • Classify work before optimising it: does it need the DOM, does it need to be consistent with the current frame, is it CPU-bound. Those three answers select the fix (When a Worker Is Actually the Answer).
  • Keep the DOM-touching part as small as possible and push everything around it outward. Parsing, filtering, sorting, diffing and transforming rarely need the document (Talking to a Worker).
  • Prefer changes the compositor can absorb for anything continuous — dragging, scrolling, transitions — so the animation is not hostage to the thread (Cheap and Expensive Animation).
  • Reduce the browser's own main-thread work, not only yours: fewer nodes, simpler selectors, contained subtrees, less script evaluated at load (CSS Containment).
  • Treat the main thread as a budget shared with every third party on the page, and account for their share explicitly rather than discovering it in field data (Third-Party Scripts and the Supply Chain).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The accessibility tree lives on the main thread with the DOM, so every main-thread cost is also an accessibility cost. This is not a secondary consequence; it is the same freeze (The Accessibility Tree).
  • Assistive technology queries and platform notifications are serviced from this thread, so a busy page is unreadable in a way that produces no visible artifact at all.
  • Work moved to a worker helps accessibility for exactly the same reason it helps rendering: the thread that answers assistive technology is free again (Web Workers and the DOM Boundary).
  • Semantics are cheap to get right and expensive to retrofit: a native control gives you keyboard behaviour, focus and a correct accessible node for free, with no main-thread cost beyond what the browser was doing anyway (Semantics Before ARIA).

What can go wrong

Failure modes
  • Moving work to a worker that then has to send back a large result, paying more in structured cloning than the work cost (Structured Clone and Transferables).
  • Assuming the compositor will save an animation that in fact triggers layout every frame, because the property animated is a geometric one (The Cost of a Change).
  • Promoting many elements to their own layers to "make it smooth" and running out of memory instead — more layers is more to composite and more to hold (Layer Explosion).
  • Optimising application code on a page whose main-thread time is dominated by style recalculation over a very large DOM, where no amount of JavaScript work will help (Selector Matching Cost).
  • Treating garbage collection pauses as unavoidable when they are caused by an allocation pattern the code chose — per-frame object churn in an animation loop is a common one (Memory Leaks).
What can arrive out of order
  • A compositor-driven animation and a main-thread-driven one can drift apart when the thread is busy, because only one of the two is affected by the delay.
  • A worker result can arrive while the main thread has already moved on, so every message handler must check whether its result is still wanted (Cancelling a Request Nobody Is Waiting For).
  • Two same-origin documents sharing a thread interleave at task boundaries, so one can observe the other mid-update if they communicate through shared storage (Auth Across Tabs).
Security
  • Same-origin documents that can synchronously reach each other typically share this thread, so one document can block another it is related to. Cross-origin isolation exists partly to make that boundary firmer (Origins and the Sandbox).
  • A worker is a thread, not a sandbox. Code in a worker runs with the same origin and can make the same network requests; moving untrusted code there buys performance isolation, not security isolation (Shared Memory and Cross-Origin Isolation).
  • The sharper shared-memory primitives require cross-origin isolation headers precisely because they enable high-resolution timing, which is a side-channel concern (The Browser Security Model).
Misreads
  • "The browser is single-threaded." The page's JavaScript and DOM are on one thread. The browser is a heavily multi-threaded, multi-process application around them (The Multi-Process Browser).
  • "Workers make things parallel, so they make things faster." They make things concurrent with the DOM thread. Whether that is faster depends on cores, on the size of the data crossing the boundary, and on whether the work was the bottleneck (Async Is Not Parallelism).
  • "If it is slow, it is my JavaScript." On many real pages the largest main-thread category is rendering work the browser does on your document's behalf.
  • "Off the main thread means safe." A worker has full network access and the same origin. It is a performance boundary and nothing more (Shared Memory and Cross-Origin Isolation).

Measuring it, and what changes in the field

How you would see this
  • The Performance panel breaks main-thread time into scripting, rendering, painting and system categories. That split is the first question to ask: which category dominates (A Mental Model of the Devtools).
  • A separate compositor track shows work that is not competing with your JavaScript, which is how you confirm an animation is actually off-thread (Debugging Rendering and Jank).
  • Worker threads appear as their own tracks, so the cost of the boundary — the serialise, the transfer, the deserialise — is visible rather than assumed.
  • Memory tooling distinguishes a leak from a working cache; garbage-collection events appear on the main-thread track and can be correlated with dropped frames (Debugging Memory).
Slow device, slow network, large data, old tab
  • On a device with few cores, moving work to a worker helps less than it does on a many-core machine, because the worker is competing for a core rather than using an idle one (Core, Hardware Thread, Software Thread).
  • During load, the main thread is at its most contended: parsing, evaluation and hydration all want it at once (The Critical Rendering Path).
  • On a very large DOM, browser-owned work — style, layout, accessibility-tree maintenance — grows faster than application work, and the levers move from JavaScript into the document (content-visibility).
  • Under memory pressure, collection work grows and interleaves more often, making frame timing less predictable in ways no application profile explains.
What this costs
  • Moving work off the main thread buys responsiveness with complexity: a message protocol, a serialisation cost, duplicated data and asynchronous seams through code that was straightforward.
  • Compositor-friendly animation is cheap because it is limited. You get transforms and opacity essentially free and pay full price for anything that changes geometry — the constraint is the mechanism, not an oversight.
  • Reducing browser-owned work usually means constraining the document — containment, virtualisation, fewer nodes — which trades flexibility in markup for predictability in rendering (CSS Containment).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThat the DOM is confined to one thread, that workers have no DOM, and that compositing can proceed without the main thread are true across engines and are the basis of every off-main-thread technique.
  • ENGINE-SPECIFICHow much is actually moved off the main thread is an implementation choice and changes between versions: engines differ in whether style, parts of layout, image decode and certain animations run on other threads, so a technique that is off-thread in one browser today may be on-thread in another or in an older version of the same one.
  • DEVICE-SPECIFICThe benefit of moving work to a worker scales with available cores: on a two-core budget phone the worker competes for the same silicon as the main thread and the win is much smaller than on a development laptop, which is exactly where the technique is usually validated.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Programming Languages & Runtime Internals — the garbage collector, the optimising compiler and the deoptimisation paths all spend time on this thread, and their behaviour explains frame drops that no application-level profile accounts for.