Tutorial · performance · Published 2026-08-16 · 3 min read

Moving work off the main thread with Web Workers

Use Web Workers to run parsing, filtering and heavy computation off the main thread, improving Interaction to Next Paint without touching the DOM.

Flat editorial illustration showing a speedometer gauge with a rising needle, clean tick marks and small trailing dots.
Illustration: this article at a glance.

Why the main thread matters

All DOM access, event handling, and rendering run on the browser's main thread, and a synchronous task on that thread blocks user input and painting until it finishes. Any single computation over about 50 milliseconds becomes a long task, and long tasks are exactly what push Interaction to Next Paint (INP) past its "good" threshold of 200 ms. When a heavy filter, a large JSON parse, or an image-pixel routine runs on the main thread, a user click that arrives during it waits: the input delay, and the INP metric, degrade.

A dedicated web worker is a background JavaScript thread with its own event loop. It runs in parallel with the main thread and cannot access the DOM, but that split is precisely the point: you move computation the DOM does not need off the UI thread, and the main thread stays free to respond to taps and paints. Because INP is measured on responsiveness rather than raw speed, the same 600 ms of work that once made an interaction feel frozen now simply happens in the background while the page stays instant. Keeping the main thread light also helps field loads on low-end devices, where main-thread contention is worst. The INP optimisation article gives the full playbook; this article is about the worker mechanism specifically.

Moving work to a worker

Create a worker by pointing a new Worker() constructor at a separate script file. That file runs in its own global scope (self rather than window). A typical carve-out is data that is user-triggered but has nothing to do with the DOM at the moment it runs:

// main thread
const worker = new Worker('/workers/filter.worker.js');

worker.postMessage(rawRecords);
worker.onmessage = (event) => {
  renderTable(event.data);
};
// inside filter.worker.js
self.onmessage = (event) => {
  const result = heavyFilter(event.data);
  self.postMessage(result);
};

Good candidates are what the web-perf consensus calls out repeatedly: large JSON or CSV parsing, client-side search indexing, data transformation and filtering, spreadsheet-style calculations, and pixel-by-pixel image math. Anything that must touch the DOM, such as reading layout or updating nodes, has to stay on the main thread, so think of workers as producing a result that the main thread then renders. Also keep the data you send bounded: postMessage deep-clones structured data, so serialising huge structures has its own cost.

Communicating results back

Where you are moving genuinely large binary buffers, use Transferable Objects. Passing a transfer list moves memory ownership to the worker instantly instead of copying, and the sender loses access to the buffer until it is returned. For a few big buffers this is a large win; for many small allocations the bookkeeping overhead is not worth it. A pattern that keeps messages cheap is to batch results rather than sending one message per row.

Two operational cautions round it out. Always terminate workers that are no longer needed (for example on component unmount) so the extra thread is released. And when a pipeline grows past a single worker, a wrapper library such as Comlink turns postMessage into something that looks like ordinary promise-based function calls, which reduces boilerplate and mistakes. To confirm a move actually paid off, record the interaction in DevTools using the Long Animation Frames (LoAF) API covered in long animation frames before and after, so you are verifying the LOAF count and INP drop rather than guessing.

Need a website built, fixed, optimised, migrated or replaced?

This technical resource is written by CSMBAC, a small design and development studio. If you would rather hand the problem to a professional, the website service page explains how we build enquiry-ready websites.

Explore website services