# Web Workers & Off-Main-Thread > Offload CPU-heavy work to a background thread, keeping the main thread free. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/web-workers-off-main-thread Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## 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. ## Definition 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 Comlink** ```ts // worker.ts import { expose } from "comlink"; const api = { async processChunk(data: Float32Array): Promise { 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(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 | Method | Copy cost | Zero-copy | Shared writes | | --- | --- | --- | --- | | **Structured clone (default)** | O(n) per call | No | No | | **Transferable (ArrayBuffer)** | O(1) | Yes — ownership moves | No | | **SharedArrayBuffer + Atomics** | O(1) | Yes — both see same memory | Yes | ## Watch out: 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. ## 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. ## Related topics - [Event Loop & scheduler.yield()](https://fearchitect.com/topics/event-loop-scheduler-yield.md): Break long tasks to keep the main thread responsive and hit INP. - [Paint & Composite Optimization](https://fearchitect.com/topics/paint-composite-optimization.md): Animate only transform and opacity to skip paint entirely. - [WebAssembly on the Frontend](https://fearchitect.com/topics/webassembly.md): Portable bytecode that runs near-native in a sandboxed browser VM. - [Realtime Dashboard](https://fearchitect.com/topics/realtime-dashboard.md): Design a live data dashboard without overwhelming the main thread. - [The Rendering Pipeline](https://fearchitect.com/topics/rendering-pipeline.md): Style → Layout → Paint → Composite: what triggers each stage. ## Further reading - [MDN — Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) - [MDN — Transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) - [MDN — SharedArrayBuffer and cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) - [Comlink (GitHub — GoogleChromeLabs)](https://github.com/GoogleChromeLabs/comlink) - [web.dev — Use web workers to run JavaScript off the browser's main thread](https://web.dev/articles/off-main-thread)