Web Workers: move heavy work off the main thread
Heavy JavaScript freezes the page because it shares a thread with the interface. A Web Worker runs that work in the background. A working example and the limits to know.
JavaScript in the browser runs on the same thread that draws the page and handles clicks. Apply a filter to a large image there and the interface freezes until it finishes: no scrolling, no button feedback, nothing.
A Web Worker is a second thread. It runs a script in the background and talks to the page through messages, so the interface stays free.
A working example
The page creates the worker, sends it the pixels and waits for the result.
// main.js
const worker = new Worker(new URL("./filter.worker.js", import.meta.url), { type: "module" });
function applyFilter(imageData) {
const { width, height, data } = imageData;
// The second argument transfers the buffer instead of copying it
worker.postMessage({ pixels: data.buffer, width, height }, [data.buffer]);
}
worker.addEventListener("message", (event) => {
drawResult(new Uint8ClampedArray(event.data));
});
worker.addEventListener("error", (event) => {
showError(event.message);
});
The worker does the slow part.
// filter.worker.js
self.addEventListener("message", (event) => {
const { pixels, width, height } = event.data;
const result = grayscale(new Uint8ClampedArray(pixels), width, height);
self.postMessage(result.buffer, [result.buffer]);
});
function grayscale(data) {
for (let i = 0; i < data.length; i += 4) {
const value = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
data[i] = data[i + 1] = data[i + 2] = value;
}
return data;
}
While the worker runs, the user can keep scrolling and clicking.
What a worker cannot do
- No DOM access. A worker cannot read or change the page. It computes and returns a result.
- No shared variables. Everything passes through
postMessage. - Messages are copied. Large objects take time to copy. Transfer an
ArrayBuffer, as above, and ownership moves without a copy.
Good candidates
- image and video processing,
- parsing large JSON or CSV files,
- building a search index,
- hashing and encryption,
- heavy calculations for charts or simulations.
A quick task is a poor candidate. Starting a worker and passing messages has a cost of its own, so save workers for work that would block the page for a noticeable time.
Practical tips
- Reuse workers. Create one and send it many jobs. Creating a worker for each task wastes startup time.
- Stop what you no longer need.
worker.terminate()ends a worker when the user leaves the screen. - Handle errors. Listen for the
errorevent, or a failure in the background goes unseen. - Consider a helper library. Comlink wraps the message passing so that calling a worker looks like calling an async function.
Workers are one way to keep a page responsive. For the others, see keeping the main thread free.