30 Days of JavaScript: Web Workers and Multithreading, Day 28

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: Web Workers and Multithreading, Day 28

Enhance your JavaScript app's performance with Web Workers. Learn how to create, manage, and optimize background threads to keep your UI responsive.

You have hit this bug even if you never named it. Click a button, the page locks up for half a second, the spinner stops spinning. That freeze is JavaScript running on one thread, and that same thread is the one drawing your interface. Hand it heavy work and the UI waits in line behind it.

Web Workers are the way out. They run a script on a separate thread so the main thread stays free to keep the page responsive. On Day 28 of our 30-day JavaScript journey, we cover what workers are, how to spin one up and talk to it, and where they actually earn their keep.

Table of Contents

Introduction to Web Workers

What are Web Workers?

JavaScript runs on a single thread. One line at a time, in order, and that thread also renders your page. A Web Worker bends that rule in a narrow, safe way: it runs a script on a real separate thread, in parallel with the main one. That is genuine parallelism, not a cleverer event loop.

The trade is isolation. A worker cannot touch the DOM, cannot read the window object, and cannot reach the variables in your main script. It gets its own global scope. The only way data moves in or out is by passing messages.

Why Use Web Workers?

Workers pay off when a task is heavy or long-running: image processing, crunching a big dataset, real-time math. Run that work on the main thread and the browser cannot paint or respond to clicks until it finishes. Move it to a worker and the UI keeps running while the numbers get crunched somewhere else.

Key Benefits:

  • The main thread stays unblocked: heavy work runs off to the side, so scrolling, clicks, and animations keep going.
  • The UI stays responsive: your app answers user input even mid-calculation.
  • Real parallelism: workers run on separate threads, so work that splits into independent chunks can run at the same time.
Types of Web Workers

There are three kinds you will run into:

  • Dedicated Workers: tied to the single script that created them. This is the common case.
  • Shared Workers: reachable from multiple scripts, even across different windows or tabs of the same origin.
  • Service Workers: sit between the app and the network as a proxy, powering offline support and background sync.

We will stick with Dedicated Workers here. They are the simplest to set up and cover most of what you actually need.

Creating and Communicating with Web Workers

Setting Up a Basic Web Worker

A worker is a separate script file plus a Worker object in your main file that points at it. Start with the worker script.

Example of a Worker Script (worker.js):

JS
// worker.js
self.onmessage = function(event) {
  const result = event.data * 2; // Simple operation for demonstration
  postMessage(result); // Send the result back to the main thread
};

The worker waits for a message, does its work (here, doubling the value it received), and posts the result back.

Instantiating and Communicating with the Worker

In your main file, create the Worker, send it data with postMessage, and listen for its reply through the onmessage handler.

Example (main.js):

JS
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
  console.log(`Received from worker: ${event.data}`);
};
worker.postMessage(10); // Send data to the worker

The main script sends 10, the worker doubles it and returns 20, and the main thread logs the result. Note the shape of it: no shared variables, just messages going back and forth.

Handling Errors in Web Workers

A worker runs in its own scope, so its errors do not surface as normal exceptions in your main code. Listen for them with the onerror handler.

Example:

JS
worker.onerror = function(event) {
  console.error(`Error in worker: ${event.message}`);
};

Without this, a failing worker fails silently. Wire it up early so you are not debugging blind.

Terminating Web Workers

A worker keeps its thread alive until you stop it. When its job is done, call terminate from the main thread to shut it down and free the resources.

Example:

JS
worker.terminate();

This kills the worker immediately. Any code still running inside it stops on the spot, so only call it once you are done with the result.

Using Web Workers with Complex Data Structures

You are not limited to numbers. You can send objects, arrays, and binary data across the boundary too.

Example:

JS
// Sending an object to the worker
worker.postMessage({ type: 'calculate', value: 42 });
// Handling the object in the worker
self.onmessage = function(event) {
  if (event.data.type === 'calculate') {
    const result = event.data.value * 2;
    postMessage(result);
  }
};

Tagging the message with a type field is a small habit worth keeping. Once a worker handles more than one kind of request, that field is how it tells them apart.

Performance Considerations

Workers are not free. Data sent across the boundary is copied, not shared, using the browser’s structured clone algorithm. For large or deeply nested objects that copy costs real time, and it happens on both ends of every message.

There is a faster path when the payload is a binary buffer. Transferable objects (an ArrayBuffer, for example) can be handed over instead of copied: ownership moves to the worker in a zero-copy operation, and the sending side can no longer use it. That turns a heavy copy into a near-instant transfer. Reach for it when you are moving big buffers around, and profile before you assume a worker made things faster.

Use Cases and Performance Benefits

Real-World Use Cases for Web Workers

Workers fit anywhere the work is heavy enough to stall the UI. A few that come up often:

  • Image Processing: filters, resizing, format conversion, all off the main thread so the page never freezes mid-edit.
  • Data Parsing and Manipulation: chewing through large datasets, parsing big JSON, running number-heavy calculations in the background.
  • Real-Time Data Processing: financial dashboards or live analytics that keep updating while the interface stays smooth.
  • Game Logic: AI or physics work in web games, moved off the render thread so the frame rate holds.
Advanced Web Worker Techniques

Once the basics click, workers pair well with a couple of other web platform pieces:

  • WebAssembly: run near-native code inside a worker for the truly heavy jobs, like video processing or complex simulations, with none of it blocking the page.
  • SharedArrayBuffer: the one exception to the copy-everything rule. A SharedArrayBuffer is actual shared memory that multiple threads read and write at once, which you coordinate safely with the Atomics operations. It comes with a real requirement, though: the page has to be cross-origin isolated (served with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp). That gate exists because shared memory was disabled after the Spectre attacks and only re-enabled behind cross-origin isolation.

Example of Web Workers with WebAssembly:

JS
const wasmWorker = new Worker('wasmWorker.js');
wasmWorker.postMessage({ buffer: wasmBuffer });
wasmWorker.onmessage = function(event) {
  console.log('Processed data:', event.data);
};

Here a worker runs the WebAssembly and the message boundary hands it the data, stacking the parallelism of one on the raw speed of the other.

Best Practices for Using Web Workers

A few rules keep workers a win instead of a wash:

  • Move less data: every message is a copy, so send only what the worker needs, and transfer buffers instead of copying them when you can.
  • Keep each worker focused: one worker, one well-defined job. A worker doing five unrelated things is harder to reason about and to error-handle.
  • Save them for the heavy stuff: the setup and message overhead only pays off on work that would actually block the main thread. Do not spin up a worker to double a number.
Case Study: Using Web Workers for Parallel Image Processing

Say you need to run a filter over a large image. On the main thread, that pass freezes the UI until it finishes. Push it to a worker and the filter runs in the background while the app stays responsive.

Example:

JS
// Main thread
const worker = new Worker('imageWorker.js');
worker.postMessage(imageData);
worker.onmessage = function(event) {
  displayImage(event.data);
};
// Worker script (imageWorker.js)
self.onmessage = function(event) {
  const filteredData = applyFilter(event.data);
  postMessage(filteredData);
};

The main thread ships the image data over, the worker filters it and sends it back, and the page never stutters. That is the whole pattern in one small example.

Conclusion

Web Workers are how you get real multithreading on the web. We covered the model (a single-threaded language plus workers running on their own threads), how to create one and message it, how to catch its errors and shut it down, and the sharper tools sitting on top: WebAssembly for near-native speed and SharedArrayBuffer with Atomics for true shared memory.

The mental model that keeps you out of trouble: workers do not share your code’s state, they trade messages, and those messages are copies unless you transfer them. Hold onto that and you can move heavy work off the main thread without your UI ever knowing it happened.

What's Next?

On Day 29 we turn to Web Security and JavaScript: the threats you will actually face, like cross-site scripting (XSS) and cross-site request forgery (CSRF), plus Content Security Policy (CSP) and other habits that keep your applications hard to break. See you there.

Next: 30 Days of JavaScript: Web Security and JavaScript, Day 29

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top