PAUL CHONGSenior Software Engineer

Browser Processes & Threads

2026-08-05 · 10 min read

Modern browsers are not single programs. Chrome runs as a collection of cooperating processes, each containing multiple threads. Understanding this architecture explains why one tab crashing doesn't kill others, why transform animations survive heavy JavaScript, and why talking between a page and a Web Worker requires postMessage.

Processes vs threads

A process is an instance of a program running in isolated memory. The OS gives each process its own address space — one process cannot read or write another's memory. If a process crashes, it takes only itself down. Starting a process is expensive; the OS allocates memory, sets up file descriptors, and initializes the runtime.

A thread lives inside a process and shares that process's memory. Threads are cheap to create. Multiple threads inside the same process can read the same variables directly — no copying, no serialization. The tradeoff: a crashing or misbehaving thread can corrupt shared memory and take the entire process down with it.

Process A (isolated memory)
├── Thread 1
├── Thread 2
└── Thread 3

Process B (isolated memory)
├── Thread 1
└── Thread 2

Communication between processes requires explicit IPC (inter-process communication) — passing messages through pipes or sockets, serializing data, paying a copying cost. Communication between threads in the same process is just a function call or shared variable — fast, but requiring careful synchronization to avoid races.

The single-process era

Early browsers ran everything in a single process: all tabs, all plugins, all network requests, the browser UI itself. This was simple but had severe consequences.

One bad tab crashed everything. A runaway while(true) loop, a memory-corrupting plugin, or a malformed page brought down every other open tab and the browser window itself.

One slow tab froze everything. Because all JavaScript ran on the same thread, a tab executing heavy computation would stall the entire browser — including the UI chrome (address bar, tabs, back button).

No security isolation. A compromised tab had access to the same memory as every other tab. Reading session cookies from a banking tab from a malicious tab was theoretically possible.

Chrome launched in 2008 with a multi-process model as a core design decision. The original Chrome comic released at launch explained the architecture to users — isolation was the headline feature.

Chrome's process model

Chrome runs several distinct types of processes. You can see them in Chrome's own task manager: ⋮ → More tools → Task manager.

Browser process

One per Chrome instance. Owns:

  • The browser UI (address bar, tabs, toolbar, settings)
  • Tab and window management
  • Navigation (deciding what URL to load, handling redirects)
  • Storage APIs (cookies, localStorage coordination)
  • Permission management

The browser process is trusted and privileged. It has full access to the OS. It orchestrates everything else but does no web content rendering itself.

Renderer process

One per site (more on this below). This is where web content lives. Every renderer process contains:

  • The HTML parser
  • The JavaScript engine (V8)
  • The CSS engine
  • The layout and paint pipeline

Renderer processes are sandboxed — they cannot make direct OS system calls, cannot write to disk, cannot open network connections directly. If a renderer is compromised, it cannot access the filesystem or other processes. Any privileged operation (file access, network request) must be proxied through the browser process.

This is the key security win of multi-process architecture: the thing executing untrusted JavaScript is isolated and unprivileged.

GPU process

One per Chrome instance. Accepts commands from renderer processes and the browser process, then communicates with the actual graphics hardware. Separating GPU access into its own process means:

  • GPU crashes (not uncommon with buggy drivers) don't take down renderers
  • Multiple renderers can share the GPU through a single gatekeeper

Network service process

Handles all network requests. Manages HTTP connections, DNS, the HTTP cache, cookies, and certificate verification. Isolated from renderers so a compromised renderer can't directly intercept or forge network traffic — it has to ask the network service, which applies security policies.

Utility processes

A catch-all for work that should be isolated but doesn't fit elsewhere: audio, storage, printing, file system access, and others depending on Chrome version. Each is sandboxed appropriately for what it does.

The full picture

Chrome
├── Browser process (1)
│   └── UI thread, IO thread, ...
├── Renderer process (per site)
│   └── Main thread, compositor thread, raster threads, IO thread
├── GPU process (1)
├── Network service (1)
└── Utility processes (several)

Threads inside the renderer

The renderer process does the most work and has the most interesting threading model. From the Browser Rendering Pipeline:

Main thread

The most important thread in the browser. Runs:

  • HTML parsing and DOM construction
  • CSS parsing and CSSOM construction
  • Style recalculation
  • JavaScript execution (V8 runs here)
  • Layout
  • Paint recording (generating display lists, not pixel-filling)

The main thread is a single-threaded event loop. Everything listed above competes for the same thread. A 200ms JavaScript task blocks layout. A forced synchronous layout blocks JavaScript. Nothing runs in parallel on the main thread — it processes one task at a time.

This is why long JavaScript tasks are so damaging. The browser cannot respond to clicks, run animations, or update the UI for as long as JS holds the thread.

Compositor thread

Runs independently of the main thread. Responsibilities:

  • Receives the layer tree and display lists from the main thread after paint
  • Coordinates raster threads to fill in pixels
  • Assembles composited layers into the final frame
  • Handles scroll and transform/opacity animations once they're handed off from the main thread

The compositor thread can produce frames completely independently of the main thread. This is the entire reason transform and opacity animations survive main thread jank — once the animation is handed to the compositor, the main thread is no longer in the loop for each frame.

Scroll is also handled by the compositor thread (in most cases). When you scroll a page, the compositor moves already-painted layers without asking the main thread at all. This is why scrolling feels smooth even on pages with heavy JavaScript.

Raster threads

A pool of threads (typically 4) that do the actual pixel work. The compositor thread breaks the page into tiles and distributes tile rasterization across the raster threads. They run in parallel.

The raster threads write pixels into GPU memory (textures). The GPU process then uses those textures to assemble the final frame.

IO thread

Handles IPC messages coming into and out of the renderer process — communicating with the browser process, network service, and GPU process. Keeps IPC from blocking the main thread.

Site Isolation

By default Chrome gives each site (scheme + registrable domain) its own renderer process, not just each tab. mail.google.com and docs.google.com run in different renderer processes even if they're open in the same tab via navigation.

This matters because of cross-site attacks like Spectre. Spectre is a CPU vulnerability that lets code read arbitrary memory within its process by exploiting speculative execution timing. If evil.com and bank.com shared a renderer process, malicious JavaScript on evil.com could potentially read memory belonging to bank.com's DOM — including session tokens, form content, page text.

With Site Isolation, those two origins are in separate processes with separate memory spaces. There is no shared memory to exploit.

Cross-origin iframes also get their own renderer process. A <iframe src="https://other.com"> inside your page runs in a different process than your page. This is why cross-origin iframes must use postMessage to communicate — they're in different processes, and direct memory access is impossible.

Web Workers

Worker threads you create from JavaScript for CPU-intensive work.

// main thread
const worker = new Worker('/worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => console.log(e.data);

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data.data);
  self.postMessage(result);
};

Web Workers run in a separate thread inside the renderer process (same process, different thread). They have no DOM access — the DOM is owned by the main thread and is not thread-safe. Communication is via postMessage, which serializes (copies) the data.

Use Workers for: image processing, parsing large JSON, cryptography, compression, physics simulations — anything that would block the main thread for more than a few milliseconds.

Shared memory without copying: SharedArrayBuffer lets a Worker and the main thread share a raw byte buffer without serialization. Requires cross-origin isolation headers (COOP/COEP) because of Spectre — shared memory between threads in the same process is exactly the attack surface Spectre exploits.

Service Workers

A persistent background thread that acts as a programmable network proxy.

// Register from the page
navigator.serviceWorker.register('/sw.js');

// sw.js — intercept all network requests
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request) ?? fetch(event.request)
  );
});

Service Workers run in their own thread, separate from any page. Key properties:

  • Persistent: survives tab close, browser backgrounding
  • No DOM access: operates at the network level, not the page level
  • Event-driven: wakes up in response to fetch events, push notifications, background sync

Because a Service Worker intercepts all fetch requests from pages under its scope, it can serve responses from cache without hitting the network — enabling offline functionality and dramatically faster repeat loads.

Service Workers also power:

  • Push notifications: the browser wakes the Service Worker to handle a push message even when no tab is open
  • Background sync: retry failed network requests when connectivity is restored

The architecture in one diagram

Tab (site: example.com)
└── Renderer Process
    ├── Main Thread
    │   JS engine, HTML/CSS parsing, layout, paint recording
    ├── Compositor Thread
    │   Layer assembly, scroll, transform/opacity animations
    ├── Raster Threads (pool)
    │   Tile rasterization → GPU textures
    └── IO Thread
        IPC with browser process, GPU process, network service

Tab (site: other.com)
└── Renderer Process (different — Site Isolation)
    └── ...

Cross-origin iframe (iframe.com inside example.com)
└── Renderer Process (different — Site Isolation)
    └── ...

Worker (created by example.com)
└── Same Renderer Process as example.com
    └── Worker Thread (no DOM access)

Service Worker (for example.com)
└── Separate Renderer Process
    └── Worker Thread (persistent, no DOM)

Why this matters for performance

The practical implications follow directly from the architecture:

Main thread is the bottleneck. JS, layout, and paint all compete here. Keep tasks short. Offload computation to Workers. Batch DOM reads and writes to minimize forced layouts.

Compositor thread is your escape hatch. Animate transform and opacity — they run on the compositor thread, off the main thread. A janked main thread does not drop these frames.

postMessage has a cost. Communicating between Workers and the main thread serializes data. For large payloads, use Transferable objects (like ArrayBuffer) which transfer ownership rather than copying, or SharedArrayBuffer for true shared memory.

Cross-origin isolation changes what APIs are available. SharedArrayBuffer and high-resolution timers require Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers. These opt the page into a stricter process model that enables safe shared memory.