A web worker is a JavaScript execution context that runs in a background thread, separate from the main thread that handles page rendering and user input. In browser JavaScript, web workers let an application perform CPU-intensive work such as parsing large files, image processing or complex calculations without freezing the interface.
A worker has its own global scope and event loop, has no access to the DOM, and communicates with the page only by exchanging messages. This isolation is what makes workers safe to use: the main thread and the worker never touch the same JavaScript objects at the same time.
How a web worker runs alongside the main thread
A page creates a worker with new Worker("worker.js"), or with { type: "module" } to load it as an ES module. The browser starts a new thread with its own JavaScript environment, loads the script and runs it independently of the page.
Communication uses `postMessage()` in both directions and a message event listener on the receiving side. Data is copied using the structured clone algorithm, which supports objects, arrays, maps, dates, typed arrays and many built-in types, but not functions or DOM nodes. Large binary data can instead be transferred as an ArrayBuffer, which moves ownership without copying and makes the buffer unusable on the sending side.
Inside the worker, the global object is self rather than window. Workers can use timers, the Fetch API, IndexedDB, WebSockets and `importScripts()` or module imports, but cannot read or modify the document, its elements or layout.
Types of workers in the browser
The term web worker usually refers to a dedicated worker, but the platform defines several worker types with different lifetimes and purposes.
| Worker type | Owned by | Lifetime | Typical purpose |
|---|---|---|---|
| Dedicated worker | One page or script | Until terminated or page closes | Offloading heavy computation |
| Shared worker | Several same-origin pages | While any connected page is open | Shared connection or state across tabs |
| Service worker | An origin and scope | Event-driven, started and stopped by the browser | Network proxy, caching, offline support |
| Worklet | A rendering pipeline | Managed by the browser engine | Audio, paint or layout hooks |
A service worker is often confused with a dedicated web worker. Both run off the main thread, but a service worker sits between the page and the network and responds to fetch and push events, while a dedicated worker simply executes code on behalf of one page.
Web workers compared with asynchronous code
Promises and async/await do not create parallelism. They let the main thread wait for input and output without blocking, but every line of JavaScript still runs on the same thread. A long loop inside an async function freezes the page just as much as a synchronous one.
| Workload | Async code on main thread | Web worker |
|---|---|---|
| Network requests | Ideal, waiting costs no CPU | Unnecessary in most cases |
| Parsing a 50 MB file | Blocks rendering and input | Runs in parallel, page stays fluid |
| Image filtering | Causes visible stutter | Suitable, especially with transfers |
| Small calculations under a few ms | Fine | Messaging overhead outweighs benefit |
| DOM updates | Required | Impossible, DOM not available |
The practical rule is to move work into a worker when a task would block the main thread for longer than about 50 ms, the threshold commonly used to define a long task. Such tasks directly hurt responsiveness metrics such as Interaction to Next Paint.
Costs and constraints of web workers
Workers are not free. Each one needs its own memory heap and startup time, typically a few milliseconds to tens of milliseconds depending on script size and device. Messages must be serialized, so passing large object graphs back and forth can cost more than the computation saved.
| Constraint | Consequence | Mitigation |
|---|---|---|
| No DOM access | Cannot render directly | Send results back for the main thread to apply |
| Copying cost of messages | Slow transfers of large data | Use transferable objects such as ArrayBuffer |
| Startup overhead | Delay before first result | Create workers early and reuse them |
| Same-origin script loading | Cannot load arbitrary remote scripts | Serve worker files from the same origin |
| Shared memory restrictions | SharedArrayBuffer needs isolation | Enable cross-origin isolation headers |
For truly shared memory, SharedArrayBuffer combined with Atomics allows several workers to operate on one buffer. Browsers only expose it on cross-origin isolated pages, because shared memory can otherwise be abused for timing attacks.
Common mistakes with web workers
- Creating a worker per task: spawning hundreds of workers wastes memory; a small worker pool sized to the device’s logical cores is more efficient.
- Forgetting to terminate: idle workers keep their memory until
terminate()is called or the page closes. - Sending functions or class instances: structured cloning drops methods and prototypes, so only plain data arrives.
- Ignoring errors: exceptions inside a worker surface as an
errorevent on the worker object and are easy to miss without a handler. - Offloading trivial work: for short tasks the messaging round trip is slower than doing the work directly.
A plain-language summary
A web page normally does everything on one track: drawing the screen, reacting to clicks and running calculations. If a big calculation takes a while, the page freezes. A web worker is a helper that works in the background on a separate track.
Picture a restaurant where the waiter also cooks. While he cooks, nobody takes orders. Hiring a cook in the kitchen means the waiter keeps serving guests while meals are prepared. The page stays responsive while the worker handles the heavy work and passes back the result.
An example from practice
A web-based spreadsheet application lets users import CSV files with several hundred thousand rows. In the first version, parsing ran on the main thread and froze the page for several seconds, during which scrolling, typing and even the cancel button stopped working.
The team moves parsing into a dedicated web worker. The file is read as an ArrayBuffer and transferred to the worker without copying, the worker parses it in chunks and posts a progress message after each chunk, and the main thread updates a progress bar. The final data is sent back in batches that the grid renders incrementally. Users can now cancel the import or keep working while it runs, and the page no longer reports long tasks during imports.