Skip to content
fearchitect
Browser & Runtime Internals

Web Workers & Off-Main-Thread

Offload CPU-heavy work to a background thread, keeping the main thread free.

By Abas TurabliReviewed

Summary

Web Workers run JavaScript on a separate OS thread, keeping CPU-intensive work — image processing, data parsing, wasm pipelines — off the rendering loop. `postMessage` copies data via structured clone or transfers `ArrayBuffer` ownership in O(1). `SharedArrayBuffer` enables shared memory but requires COOP/COEP headers to activate. Comlink wraps workers in a Promise-based RPC layer, removing postMessage boilerplate.

Jump to the interview angle

A Web Worker is a script that runs on a background OS thread, separate from the main thread that handles rendering, event dispatch, and JavaScript execution. Because each thread has its own event loop, long tasks in a worker cannot block painting or input handling. Workers communicate with the main thread only through postMessage, which copies data via structured clone or transfers ownership of binary buffers. Workers have no access to the DOM.

Comlink worker — ergonomic RPC over postMessage

Comlink (Google Chrome Labs) wraps a worker class so callers await methods as if they were local async functions, eliminating manual postMessage/onmessage wiring.

worker.ts + main.ts with Comlinkts
// worker.ts
import { expose } from "comlink";

const api = {
  async processChunk(data: Float32Array): Promise<number> {
    let sum = 0;
    for (let i = 0; i < data.length; i++) sum += data[i];
    return sum;
  },
};

expose(api);

// main.ts
import { wrap } from "comlink";

const worker = new Worker(new URL("./worker.ts", import.meta.url), {
  type: "module",
});
const remote = wrap<typeof api>(worker);

// Awaiting a worker method — no postMessage plumbing needed.
const result = await remote.processChunk(new Float32Array([1, 2, 3]));
console.log(result); // 6

expose registers the object inside the worker; wrap returns a Proxy on the main thread. Each method call becomes a postMessage round-trip under the hood, typed end-to-end via TypeScript generics.

Transferring data across the thread boundary

MethodCopy costZero-copyShared writes
Structured clone (default)O(n) per callNoNo
Transferable (ArrayBuffer)O(1)Yes — ownership movesNo
SharedArrayBuffer + AtomicsO(1)Yes — both see same memoryYes

SharedArrayBuffer requires COOP + COEP headers

Browsers disable SharedArrayBuffer unless the page is cross-origin isolated: send Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on every document response. Without them SharedArrayBuffer is undefined at runtime, even in modern browsers. Verify with self.crossOriginIsolated === true before using it.

When a worker is worth the overhead

  • Any task taking **>50 ms** on the main thread is a candidate — that's one dropped frame at 60 fps.
  • Image or video processing, wasm modules, large JSON parse/stringify, and crypto hashing are natural fits.
  • **Avoid** workers for small, infrequent tasks — thread startup (~5 ms), serialization, and proxy overhead add up.
  • Use a **worker pool** (e.g. `workerpool`) to amortize startup across repeated calls.
  • Workers cannot touch the DOM; pass back computed values and let the main thread apply them.

Interview angle

Interviewers ask why the main thread matters and how to offload work without blocking it. Strong answers name the structured-clone vs transferable distinction, mention Comlink for ergonomics, and explain the COOP/COEP requirement for SharedArrayBuffer.

Soundbite: "Transfer ArrayBuffer ownership in O(1) for data pipelines; reach for SharedArrayBuffer only when both threads write, and only once you've set COOP/COEP headers."

Key terms

Structured clone
Default postMessage serialization — deep-copies the value between threads, O(n) in data size.
Transferable
An object (e.g. `ArrayBuffer`) whose ownership moves to the receiver thread in O(1) with zero copy.
SharedArrayBuffer
A fixed-length binary buffer visible to both threads simultaneously; writes on one are visible on the other.
Atomics
Built-in API for lock-free, thread-safe operations on `SharedArrayBuffer` — `Atomics.wait`, `Atomics.notify`.
Comlink
Library that wraps a Worker with a Proxy, exposing its methods as awaitable async functions via postMessage RPC.

Further reading

Search fearchitect

Jump to a topic, mode, or action.