Why move a calculation to a worker?
The browser’s main thread handles much of an interface’s JavaScript alongside work needed to respond to input and update the page. A long, synchronous calculation can prevent a button from responding or a progress message from appearing until the calculation finishes. A Web Worker runs JavaScript in a separate thread and communicates with the page through messages, allowing the main thread to remain available for interface work.
Workers are useful for tasks such as processing a large data set or performing repeated calculations. They are not a shortcut for every slow page: creating a worker and exchanging messages have costs, and a worker still consumes computing resources. Start by identifying work that takes a noticeable, uninterrupted stretch of CPU time. If the delay instead comes from waiting for a network response, moving the request into a worker may not address the underlying problem.
Set up a small, reproducible example
For this example, use a modern browser and serve three files from the same directory over a local HTTP server: a page, main.js, and worker.js. The page needs a number input with the id number, a button with the id run, and an element with the id result. Load main.js as a JavaScript module. The task is to count prime numbers up to the entered limit; it is deliberately simple so the thread boundary is easier to see.
In main.js, create the worker with const worker = new Worker('./worker.js', { type: 'module' });. Select the three page elements with document.querySelector. In the button’s click handler, turn the input into a finite whole number, constrain it to a sensible range for the example, set the result text to “Working…”, and call worker.postMessage({ limit });. Add a message listener that sets the result text to String(event.data.count). Add an error listener that displays a failure message rather than leaving “Working…” on screen indefinitely.
In worker.js, assign a handler to self.onmessage. Read event.data.limit, set count to zero, and test each integer from 2 through limit. For each integer n, try divisors from 2 while divisor * divisor <= n; if one divides n evenly, n is not prime. Increment count for each number that remains prime, then call self.postMessage({ count });. The worker performs the loop, while the page owns the input and output. Keeping those responsibilities separate makes the message flow easier to inspect and debug.
Understand the boundary between page and worker
A dedicated worker cannot directly read the page’s elements or change their text. That is why the main script collects the input and renders the answer. Messages generally use structured cloning, so ordinary objects and arrays can be sent without manually converting them to strings. The receiving side gets its own data rather than a shared reference to the original object.
For large binary data, copying can itself become expensive. Some objects, including an ArrayBuffer, can instead be transferred with postMessage’s transfer list. Transfer gives the receiving side ownership and makes the sender’s buffer unusable. Use it when that ownership change fits the task; do not transfer data the page still needs to read. Also keep messages focused: sending a small input and a small result is simpler than repeatedly passing a large data set back and forth.
Handle repeated requests and cancellation
The prime-counting example has a subtle interface issue: someone can press Run again before the first answer arrives. If requests overlap, an older result may arrive after a newer one. Include an incrementing requestId in each message and have the worker return it with the result. The main script should display a result only when its requestId matches the most recent request. This protects the interface from stale answers, although it does not stop unnecessary computation.
For a task that must stop immediately, worker.terminate() ends that worker; create a new one before starting another calculation. Sending a “cancel” message is not enough to interrupt a worker that is busy inside one long synchronous loop: it cannot handle that message until the loop yields. For finer control, divide the work into batches so the worker can process messages between batches. Decide which approach suits the task before adding progress controls or a cancel button.
Check whether the change helps
Try the example with a limit large enough to notice, then interact with the page while it runs. The main thread should remain better able to respond than it would with the same loop in a button handler, but the total calculation may not finish sooner. Keep limits on user-supplied work, report failures, and avoid starting an unbounded number of workers.
A worker is most valuable when the page can hand it a self-contained job and continue serving the user. Measure the behavior that matters for your interface—such as whether input remains usable—rather than treating the presence of a worker as a performance improvement by itself.



