The Multi-Process Browser
Browser, renderer, network, GPU and utility processes — why a page can hang without taking the browser with it, and what that means for your code.
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.
When my page freezes, what exactly is frozen, and what is still running?
A person has fifteen tabs open. One of them is a heavy application. They expect the other fourteen — and the browser itself — to keep working.
The browser is one program. If a page hangs, the browser hangs; if it crashes, everything is lost.
A hung page in a modern browser leaves the tab strip, the address bar and every other tab responsive. Something is clearly not sharing a thread with your JavaScript.
- A hung page in a modern browser leaves the tab strip, the address bar and every other tab responsive. Something is clearly not sharing a thread with your JavaScript.
- "Out of memory" can mean the renderer for one origin, not the machine. The failure is scoped in a way that a single-process model cannot explain.
- Some APIs only work in a "cross-origin isolated" context. That requirement makes no sense unless process boundaries are load-bearing for security (Shared Memory and Cross-Origin Isolation).
- Network requests continue while the main thread is blocked, and images decode while script runs. Neither is possible if there is only one thread of execution in the browser.
What is actually happening
In the browser, not in the framework.
- The browser process owns the UI you do not control: tab strip, address bar, menus. It coordinates everything else and it is the one whose responsiveness the user reads as "the browser is fine".
- Renderer processes run pages: parsing, style, layout, paint, and your JavaScript on the main thread. Chromium isolates by site by default, so different origins usually get different renderers.
- A network process handles requests, connection reuse and the HTTP cache for everyone, which is why fetching continues while a page's main thread is busy.
- A GPU process performs compositing and rasterisation, which is why a CSS animation on a composited property can keep running while the main thread is blocked (Cheap and Expensive Animation).
- Utility processes handle things worth isolating: audio, storage, extensions, media decoding.
- Inside a renderer there are still several threads — compositor, raster, worker threads — but exactly one of them owns the DOM (What the Main Thread Owns).
What this makes the browser do
And which of it is avoidable.
- Every process boundary costs memory: each renderer carries its own engine instance and heap. Isolation is bought with RAM, which is why browsers bound the process count on low-memory devices.
- Cross-process communication is message passing, not shared memory. It is fast but not free, and it is asynchronous.
- Under memory pressure, the browser may discard a background tab's renderer entirely and re-create it on return — your page can be silently torn down and rebuilt (Persistent Client State).
What is isolated from what
The reason to know this is not trivia. It answers a specific class of question: when something is stuck, what is stuck with it? Your JavaScript, DOM mutation, style and layout share a thread and stop together. Network, compositing and workers do not.
This is also why the advice "move it off the main thread" is meaningful rather than a slogan. There is somewhere else for it to go, and the browser is already using those places on your behalf.
- Blocked main thread: no input handling, no DOM updates, no style, no layout, no accessibility-tree updates.
- Still running: network requests, image decode, compositor-driven animation on composited properties, worker threads, other tabs.
- A renderer crash is scoped to its site. A browser-process crash is not scoped to anything.
Your page can be torn down without asking
A page has a lifecycle beyond load and unload. It can become hidden, be frozen, be discarded to reclaim memory, and later be restored — sometimes from the back/forward cache with its JavaScript state intact, sometimes from scratch with nothing at all.
The practical rule that falls out of this: persist user-created state when it is created, not when the page is leaving. The events that fire on the way out are the least reliable ones in the platform, and a discarded renderer fires none of them.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
Saving a draft in an unload handler | Drafts lost, unreproducibly | unload is not guaranteed to run — and does not run at all when a renderer is discarded | Save on change, debounced; treat leaving as a flush, never as the save (Persistent Client State). |
A setInterval poll in a background tab | Data is stale on return; reconnect storms when many tabs wake at once | Background timers are throttled aggressively, then all fire on foreground | Poll on visibility change, with jitter on resume (Reconnect and Backoff). |
| Assuming module state survives a return to the tab | App reloads to an empty state, losing context | The renderer was discarded; the page was rebuilt from the URL | Make the URL and durable storage the source of truth for anything worth restoring (The URL Is Application State). |
| Assuming a return to the tab means a fresh page | Duplicate listeners, resumed timers, stale WebSocket handling | A bfcache restore resumes the same JavaScript state, it does not re-run it | Handle pageshow/pagehide explicitly and resynchronise on resume (Resynchronisation After a Gap). |
How to build it
Most important first.
- Design for a page that can be frozen, backgrounded, discarded or restored at any moment. Persist anything the user would be upset to lose, at the moment they create it rather than on unload (Persistent Client State).
- Understand which work survives a blocked main thread — network, compositor-driven animation, worker threads — and put anything that must keep responding there (When a Worker Is Actually the Answer).
- Do not rely on
unloadto save state. It is unreliable by design and is not run when a renderer is discarded; use visibility change instead. - Treat cross-origin isolation as an architectural decision with consequences, not a header you add casually: it changes what third-party content can be embedded at all.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- The accessibility tree is computed in the renderer and exposed to platform assistive technology across a process boundary. When the renderer's main thread is blocked, that tree stops updating — a screen reader will read stale content without any indication it is stale.
- This is the strongest accessibility argument for keeping the main thread free: a visual user sees a frozen page and knows to wait, while a screen-reader user may be told something that is no longer true.
- Tab discard and restore can reset focus to the document. Restoring a sensible focus target after a restore is the difference between a keyboard user continuing and starting over (Focus Management).
What can go wrong
- A renderer crash takes every page in that process with it — usually one site, occasionally more if processes were merged under memory limits.
- Assuming a backgrounded tab keeps running: timers are throttled aggressively and the tab may be discarded entirely.
- Assuming state survives a bfcache restore or a discard-and-restore cycle. It sometimes does and sometimes does not, and code that only works in one case is a bug waiting for a low-memory device.
- Assuming a
SharedArrayBufferis available. It requires cross-origin isolation, which most pages do not have and cannot casually adopt.
- Cross-process messages are asynchronous. State read in the renderer can be stale relative to the browser process — visibility, focus and network status all arrive as events, not as facts.
- A discard-and-restore can interleave with in-flight requests: a response can arrive for a page that no longer exists in the form that sent it.
- Process isolation is a security boundary, not only a stability one. Putting different sites in different processes means a memory-safety bug in the renderer does not automatically expose another site's data.
- This is the mechanism behind the mitigations for speculative-execution attacks: if the attacker's code and the victim's data are never in the same address space, a timing side channel has nothing to read (Origins and the Sandbox).
- The renderer is deliberately unprivileged. It cannot touch the filesystem or the network directly; it asks the browser and network processes, which check (The Browser Security Model).
- Cross-origin isolation exists because sharing memory between threads re-enables precise timers, which re-enables those side channels. The headers are the price of the capability.
- "The browser is single-threaded." One thread per page owns the DOM. The browser as a whole is a multi-process system with many threads.
- "If my page freezes, the browser freezes." Not since multi-process browsers became standard — which is precisely why a frozen page can go unnoticed by everyone except the person using it.
- "Web Workers are separate processes." They are separate threads, usually within the same renderer process. The DOM boundary is the point, not the process boundary (Web Workers and the DOM Boundary).
- "A tab left open keeps running." It may be throttled, frozen, or discarded and rebuilt from scratch.
Measuring it, and what changes in the field
- The browser's own task manager attributes CPU and memory per process, which is how you tell "our tab is heavy" from "the machine is heavy".
- The Performance panel shows the main thread and compositor thread separately — the clearest way to see that an animation kept running while script was blocked.
- Field data on tab discards and restores is worth collecting if your application holds unsaved user state; it is invisible in local testing (Release Health).
- On low-memory devices the browser merges sites into fewer processes, so isolation guarantees you observed on a desktop may not hold on a phone.
- With many tabs open, background tabs are throttled and discarded far more aggressively — the state of a long-running dashboard left in a background tab is genuinely uncertain.
- A long-lived tab may be restored from a discarded state hours later, running your old code against a backend that has since been deployed twice (Long-Lived Clients and Version Skew).
- Process isolation costs memory — substantially. Browsers trade security and stability for RAM, and on constrained devices they take some of it back.
- Cross-origin isolation buys precise timing and shared memory, and costs you the ability to embed most third-party content without explicit opt-in from that third party.
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.
- BROWSER-SPECIFICThe process split is an implementation choice, not a specification. Chromium isolates per site by default with a per-device process ceiling; Firefox uses a bounded pool of content processes with site isolation layered on; Safari isolates per tab with its own arrangement. Never write code that depends on a particular split.
- SIMPLIFIEDThe five-process sketch omits real detail: renderers host multiple frames, out-of-process iframes complicate the picture, and process allocation adapts to available memory. It is accurate about what is isolated from what, which is the part that matters to application code.
- DEVICE-SPECIFICOn low-memory devices browsers reduce process counts and discard background tabs far sooner, so isolation and persistence behaviour observed on a desktop does not transfer to a phone.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Operating Systems — process isolation, address spaces and the scheduler that decides which of these processes runs at all.