JavaScript is single-threaded. That one fact explains half of all janky web apps: parse a big JSON file, crunch pixels, or run a search across ten thousand files on the main thread, and your UI freezes until it's done. The browser's answer is Web Workers — real OS-level threads you can spin up from JavaScript.
Here's the part most tutorials skip: workers aren't just "background threads." Combined with SharedArrayBuffer, transferable objects, and OffscreenCanvas, they form a complete multithreading toolkit. Every example below runs as-is — paste it into a NitroIDE preview (which is a real browser iframe) and watch it work.
You don't even need a separate file. Create a worker from a Blob URL inline:
The main thread never blocks. Animations keep running, clicks keep registering. That alone fixes most "the page froze" bugs.
Real-world proof: The Monaco editor inside NitroIDE runs its TypeScript language features in a web worker. Autocomplete, hover info, and error checking all happen off the main thread — which is why typing stays instant even in large files.
By default, postMessage clones your data (structured clone). Sending a 100 MB image buffer to a worker means copying 100 MB. Transferables fix that — ownership of the buffer moves to the worker with zero copying:
Rule of thumb: if the buffer is bigger than a few hundred KB, transfer it. Cloning is fine for small JSON-ish messages.
Sometimes transfer isn't enough — you want two threads reading and writing the same memory, like a progress counter or a streaming log. That's SharedArrayBuffer plus Atomics:
Two honest caveats. First, SharedArrayBuffer requires cross-origin isolation — your page must serve the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers, or the constructor throws. Second, Atomics.wait is only allowed inside workers; calling it on the main thread throws. Design accordingly: the main thread posts messages, workers do the waiting.
Canvas drawing on the main thread competes with layout, input, and your framework. OffscreenCanvas moves rendering into a worker — perfect for minimaps, visualizations, and live previews:
Note: inside a worker there's no requestAnimationFrame on window — but OffscreenCanvas contexts support their own animation loop patterns, and a simple setInterval-driven or message-driven render loop works fine.
When a worker produces data faster than the main thread consumes it (think high-frequency logs or sensor streams), a ring buffer over shared memory is the classic answer — the writer never blocks, the reader never misses:
Workers have startup cost (a few ms) and every message crosses a serialization boundary. Don't use them for tiny tasks, DOM access (workers have no DOM — that's the point), or anything that needs synchronous return values. A good rule: if it takes less than ~16 ms, keep it on the main thread; if it can jank a frame, move it.
Try it now: NitroIDE's preview pane is a real browser iframe with full worker support. Paste the Blob-worker example above into a new HTML file, open the console, and confirm the page stays responsive while 50 million square roots get crunched in the background.
Prototype worker-based architectures instantly — no build step, no server, just your browser.
Launch NitroIDE