cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Web Workers

Learn how Web Workers keep browser interfaces responsive by running CPU-heavy JavaScript on another thread, passing structured-clone messages, and using transferables safely.

You will be able to
  • 01
    Move heavy work off the UICreate a worker, post work to it, receive results, handle errors, and terminate it when it is no longer needed.
  • 02
    Send the right dataExplain structured clone, why functions and DOM nodes fail, and when transferables avoid copying.
  • 03
    Choose workers deliberatelyCompare classic workers, module workers, worker-safe APIs, and tasks that should stay on the main thread.

The UI needs its waiter

A browser page has a main thread: the thread that runs most of your JavaScript, responds to input, updates the DOM, calculates layout, and helps the browser paint. If you keep that thread busy with a long calculation, the page feels stuck.

A Web Worker is a separate JavaScript environment that can run code on another thread. A dedicated worker belongs to the page that created it. The page and the worker do not share variables or the DOM; they send messages with postMessage.

Real-life analogyA restaurant with one waiter and a kitchen helper

Imagine a small restaurant with one waiter. If the waiter leaves the dining room to chop onions for a minute, every table waits. Hiring a kitchen helper fixes that: the waiter still talks to customers while the helper does heavy prep.

In real life: The only waiter keeps serving tables
In JavaScript: The main thread keeps the UI responsive
In real life: The helper chops vegetables in the back
In JavaScript: A worker runs CPU-heavy JavaScript away from the UI
In real life: The helper never talks to customers
In JavaScript: Workers cannot touch the DOM or call document.querySelector
In real life: Tickets pass through the kitchen window
In JavaScript: Messages pass through postMessage

Where the analogy stops: A real helper can look into the dining room. A worker cannot reach the page's DOM at all; it must ask the main thread to update the interface.

This lesson covers dedicated workers, message passing, structured clone, transferable objects, and module workers. If the words “task” and “event loop” feel fuzzy, keep the Event loop lesson nearby.

Creating and stopping a dedicated worker

FOUNDATION

A dedicated worker is created with new Worker(url). In a production app, your bundler often gives you that URL. In this lesson, each playground creates a safe predefined Blob URL so the code runs on this page without fetching external files.

Small worker shapePop out in the code editor (opens in a new tab)JavaScript
// main.jsconst worker = new Worker(workerUrl); worker.postMessage({ type: "count", limit: 120000 }); worker.onmessage = (event) => {  renderResult(event.data);}; worker.onerror = (event) => {  console.error(event.message);}; // worker.jsself.onmessage = (event) => {  const result = doHeavyWork(event.data.limit);  self.postMessage(result);};

The worker global object is self. Dedicated workers have postMessage, onmessage, timers such as setTimeout, fetch, IndexedDB, and more. They do not have document, page window, layout, or direct access to React state.

Always clean up workers

A worker can keep running after the button that started it is no longer useful. Call worker.terminate() when the work is stale, when the user resets the experiment, or when a component unmounts. terminate() stops the worker immediately.

Jank lab: move prime counting off the UI

INTERACTIVE

“Jank” is the feeling of a page missing frames. Counting primes is a friendly CPU task because it uses only JavaScript and no network. Run it on the main thread first: the moving indicator freezes because the waiter is chopping onions. Then run the same calculation in a worker.

Jank lab: count primes without freezing the waiter
Same task, two homesPop out in the code editor (opens in a new tab)JavaScript
function countPrimesUpTo(limit) {  let count = 0;  for (let value = 2; value <= limit; value += 1) {    if (isPrime(value)) count += 1;  }  return count;} // Main thread: blocks clicks, animation, and paint until done.const total = countPrimesUpTo(120000); // Worker thread: the same CPU work runs away from the UI.worker.postMessage({ type: "count", limit: 120000 });
Smooth indicator120,000
  • Run the main-thread version, then the worker version. Watch the moving bar.
Try it yourself

The main-thread button deliberately blocks JavaScript. The worker button creates a Blob URL worker, posts the same task, and terminates it after the reply. Durations vary by machine.

Keep the number modest: the goal is a visible pause, not a laptop fan test.

The worker version still uses CPU. It is not magic. The win is that the main thread can keep handling input and paint while the helper thread works. Do not create workers for every small loop; startup and message costs are real.

Messages and structured clone

INTERACTIVE

The page and worker communicate by message events. You call postMessage(value); the other side later receives an event whose data is the message. The value is copied with the structured clone algorithm. It understands more than JSON: Date, Map, Set, ArrayBuffer, nested arrays, and many plain objects.

Real-life analogyOrder tickets through the kitchen window

The kitchen helper does not grab the waiter’s notebook. The waiter tears off a ticket and passes that copy through the window. If the waiter changes the notebook after that, the kitchen still cooks from the old ticket.

In real life: The waiter writes an order ticket
In JavaScript: The main thread builds a message object
In real life: The helper receives a copy of the ticket
In JavaScript: The worker receives structured-cloned data
In real life: Changing the waiter's notebook later does not edit the ticket
In JavaScript: Mutating the original after postMessage does not mutate the copy

Where the analogy stops: A paper ticket can contain a drawing of anything. Structured clone has rules: functions, symbols, and DOM nodes are not cloneable.

Message lab: copy the order ticket
Structured clone inputsPop out in the code editor (opens in a new tab)JavaScript
const original = {  created: new Date("2026-09-25T08:30:00.000Z"),  labels: new Map([["level", "warm-up"]]),  nested: ["ticket", { count: 1 }],}; worker.postMessage(original);original.nested[1].count = 99; // The worker receives a structured-clone copy with count still 1.worker.postMessage(() => "functions cannot be cloned");
Message logreal worker
  • Post a rich object and compare the worker's copy with the changed original.
Try it yourself

Structured clone preserves many built-in data types, but it sends a copy. Later mutations to the original object do not change the worker's message.

The caught DataCloneError is the browser refusing to clone a function.
Structured clone is not JSON

JSON turns everything into text and loses types such as Date and Map. Structured clone keeps many browser and JavaScript data types, but it still cannot copy behavior such as functions. A bad message throws DataCloneError before it reaches the worker.

Transferables: hand over the pot

INTERACTIVE

Copying a huge binary buffer can be wasteful. Some objects are transferable: ownership moves to the receiver instead of copying bytes. Transferables include ArrayBuffer, MessagePort, ImageBitmap, and OffscreenCanvas. After you transfer an ArrayBuffer, the sender’s buffer is detached and its byteLength becomes 0.

Real-life analogyThe actual pot, not a photocopy

If the kitchen needs a recipe, a photocopy is fine. If it needs the heavy soup pot, making another pot is silly. Hand over the real pot, but remember the waiter cannot keep serving from it afterward.

In real life: Photocopying a recipe
In JavaScript: Structured clone copies data
In real life: Handing over the actual soup pot
In JavaScript: Transfer moves ownership of a transferable object
In real life: The waiter no longer has the pot
In JavaScript: The sender's transferred ArrayBuffer is detached

Where the analogy stops: Transferring is great only when the sender truly no longer needs that object. If both sides need independent data, copy instead.

Transfer lab: photocopy or hand over the pot
Copy versus transferPop out in the code editor (opens in a new tab)JavaScript
const buffer = new ArrayBuffer(4 * 1024 * 1024); worker.postMessage({ buffer });console.log(buffer.byteLength); // sender still owns it const fastBuffer = new ArrayBuffer(4 * 1024 * 1024);worker.postMessage({ buffer: fastBuffer }, [fastBuffer]);console.log(fastBuffer.byteLength); // 0: ownership transferred
Byte lengths4 MB
  • Compare copying bytes with transferring ownership.
Try it yourself

Without a transfer list the sender keeps its buffer. With a transfer list the sender's byteLength becomes 0 because the bytes moved to the worker.

Transfer can be much faster for large binary data, but only when giving up the sender's copy is correct.

SharedArrayBuffer is the special case for shared memory. It requires cross-origin isolation in browsers, so most lessons and many apps should treat it as an advanced deployment topic rather than a default worker tool.

Classic workers and module workers

INTERACTIVE

Classic workers load classic scripts. Inside a classic worker, importScripts("helper.js") can synchronously load more classic scripts. Module workers are created with new Worker(url, { type: "module" }). They use module loading rules, so they can use import and export in browsers that support them.

Module worker lab: detect by trying
Classic versus modulePop out in the code editor (opens in a new tab)JavaScript
const classic = new Worker(classicUrl);// classic workers use importScripts("helper.js") when they need scripts. try {  const moduleWorker = new Worker(moduleUrl, { type: "module" });  moduleWorker.postMessage("ping");} catch (error) {  console.log("module workers are not available here");}
Feature checkBlob URL
  • Try a classic worker and a module worker in this browser.
Try it yourself

A classic worker loads classic scripts and can use importScripts. A module worker participates in ES module loading and is requested with { type: 'module' }.

Feature detection is a try/catch around construction plus an error handler; support and restrictions depend on the browser and page policy.

The worker kind does not change the big safety rule: neither kind gets DOM access. The choice is mostly about how code is loaded and bundled. In real projects, check your bundler’s worker URL pattern and your site’s Content Security Policy. This site sends no CSP that blocks blob: workers, so these experiments can use Blob URLs.

The message timeline

STEP THROUGH

postMessage is not a function call into the worker. It queues a message. The current script continues, microtasks still follow the usual rules from the Promises and event-loop lessons, and the worker’s onmessage runs later in the worker’s own event loop. A reply comes back as a later message event on the Worker object.

Step through the message timeline
Step 0 of 7Ready
Your turn: follow the blue line

Trace the real message timeline: the main thread posts, the worker handles the message later in its own event loop, and the reply returns as another message.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
console.log("main: before postMessage");worker.postMessage(ticket);ticket.items.push("pie");console.log("main: changed original"); // later, in the worker event loopself.onmessage = (event) => {  console.log(event.data.items.join(", "));  self.postMessage("done");};
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Message style

Predict which values the main thread sees before the worker message runs.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Where workers fit in real interfaces

PRACTICAL

Workers are best for work that is CPU-heavy, independent from the DOM, and easy to describe as input data plus output data: parsing big files, searching many records, generating thumbnails, compressing images, crunching numbers, or sorting a very large table. A normal fetch already avoids blocking the main thread; a worker helps only if the response needs heavy processing afterward.

Sorter pattern: pick the right home for work
Worker sorter shapePop out in the code editor (opens in a new tab)JavaScript
const names = ["Grace", "Ada", "Linus", "Brendan", "Hedy"]; // Small list: keep it simple.const sortedHere = [...names].sort((a, b) => a.localeCompare(b)); // Large list: send a copy and render the reply.worker.postMessage(names);worker.onmessage = (event) => renderRows(event.data);
Rows5 names
  1. Grace
  2. Ada
  3. Linus
  4. Brendan
  5. Hedy
Try it yourself

Sort a small list on the main thread or send it to a worker. For tiny arrays, the worker is educational overhead.

Workers shine when the list is huge or the comparison work is expensive. Small UI sorts are usually simpler on the main thread.
Main thread or worker?
  • Update button text after a click
  • Parse a 20 MB export file before showing a summary
  • Render pixels with OffscreenCanvas when supported
  • Fetch JSON while the user waits
  • Hide API keys from the user
  • Sort 100,000 rows for a table
  • Call alert() from background code
  • Format six dates for a settings page
Try it yourself
0 of 8 correct

Sort each job by the best home. Think about DOM access, work size, and whether the browser API is already asynchronous.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Main thread versus worker thread
FeatureMain threadWorker thread
Can touch DOMYes: document, layout, events, paint coordinationNo: no document or window DOM
Good atSmall UI updates, event handlers, starting async workCPU-heavy calculations and parsing
CommunicationDirect calls inside the pagepostMessage and message events
StoppingReturn from your functionworker.terminate() stops immediately

Common misconceptions

  • “Workers can update the page.” They cannot touch the DOM. Send a message back and let the main thread update UI.
  • “Workers make code faster automatically.” They make the UI more responsive. Total wall time may improve, stay similar, or even get worse for small tasks.
  • “Messages share objects.” Normal messages copy with structured clone. Later changes to the original object are separate.
  • “Transfer is just a faster copy.” Transfer moves ownership. The sender loses that buffer.
  • “A worker hides client secrets.” Worker code and messages are still in the user’s browser.
  • “Service workers are the same thing.” Dedicated workers help a page compute. Service workers sit between pages and network requests; that is the next lesson.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the structured clone

Predict what the two lines print when an object is cloned and then the original is changed.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const original = { nested: { count: 1 } };
const copy = structuredClone(original);
original.nested.count = 9;
console.log(copy.nested.count);
console.log(original.nested.count);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeChoose the worker boundary

    Sketch the main-thread and worker responsibilities for sorting table data.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Goal: keep the UI responsive while processing rows.
    worker.postMessage(rows);
    worker.onmessage = (event) => renderRows(event.data);
      Exercise 3 · PracticeFind the transfer result

      Predict the byte lengths after an ArrayBuffer transfer.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const buffer = new ArrayBuffer(4);
      const copy = structuredClone(buffer, { transfer: [buffer] });
      console.log(buffer.byteLength);
      console.log(copy.byteLength);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the bug: DOM in the worker

        Answer the instant check, then fix the code so the page updates correctly.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        // worker.js
        self.onmessage = () => {
          const status = document.querySelector("#status");
          status.textContent = "Done";
        };

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeOrder the reply

          Predict which line prints second when a worker reply competes with current script and a microtask.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log("main start");
          queueMicrotask(() => console.log("main microtask"));
          setTimeout(() => console.log("worker reply event"), 0);
          console.log("main end");

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Web Workers quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What problem does a Web Worker solve?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this structured-clone example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const original = { nested: { count: 1 } };
              const copy = structuredClone(original);
              original.nested.count = 9;
              console.log(copy.nested.count);
              console.log(original.nested.count);

              Choose an answer to see the explanation.

            3. Question 3 of 8Which value cannot be sent with structured clone?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this ArrayBuffer transfer print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const buffer = new ArrayBuffer(4);
              const copy = structuredClone(buffer, { transfer: [buffer] });
              console.log(buffer.byteLength);
              console.log(copy.byteLength);

              Choose an answer to see the explanation.

            5. Question 5 of 8What happens when you call worker.terminate()?

              Choose an answer to see the explanation.

            6. Question 6 of 8Which API is available inside many dedicated workers?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which statement about module workers is most accurate?

              Choose an answer to see the explanation.

            8. Question 8 of 8What order does this worker-reply model print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("main start");
              queueMicrotask(() => console.log("main microtask"));
              setTimeout(() => console.log("worker reply event"), 0);
              console.log("main end");

              Choose an answer to see the explanation.

            Key takeaways

            • Dedicated workers let a page run CPU-heavy JavaScript away from the main UI thread.
            • Workers communicate with postMessage; normal messages are structured-clone copies.
            • Transferables move ownership, so a transferred ArrayBuffer becomes detached on the sender side.
            • Workers have useful APIs such as timers, fetch, and IndexedDB, but no DOM.
            • Terminate workers when their work is stale, and listen for error events.

            Final definition.
            A Web Worker is a browser-managed JavaScript environment that runs separately from the page’s main thread and communicates by message events.

            Up next: Service workers & offline apps.

            CompleteFrontend Clear concepts. Working examples.