Lesson 0002 · ~12 minutes

Dedicated workers: lifecycle & messaging

One skill: design a correct dedicated-worker loop — create, message, handle errors, shut down — without treating it like a shared-memory thread.

Win for this lesson

You can sketch a component-safe worker client (mount → postMessage → onmessage → terminate) and explain copy-not-share messaging in a design review.

1. Two worlds, one channel

A Worker is a handle on the main thread to a script running in DedicatedWorkerGlobalScope. Per MDN and the WHATWG intro:

Spec non-normative guidance that matters for design: workers are relatively heavy (startup + memory). Prefer long-lived workers you reuse, not one worker per pixel or per keystroke.

2. The asymmetric API (easy to botch)

MDN’s crucial note: on the main thread you hang postMessage / onmessage on the Worker object; inside the worker they live on the global scope, because the worker is the global.

// main.js
const worker = new Worker("worker.js");

worker.postMessage({ type: "sum", a: 40, b: 2 });

worker.onmessage = (e) => {
  console.log(e.data); // { type: "sum", result: 42 }
};

worker.onerror = (e) => {
  console.error(e.message, e.filename, e.lineno);
};

// later / unmount
worker.terminate();
// worker.js
onmessage = (e) => {
  const { type, a, b } = e.data;
  if (type === "sum") {
    postMessage({ type: "sum", result: a + b });
  }
};

// optional: worker decides it's done
// close();
Design habit: name your messages

The platform is fire-and-forget events, not RPC. Once you send more than one kind of job, put a type (and usually a correlation id) on every message so the main thread can match answers to waiting UI work. Libraries like Comlink hide this; you should still understand the wire shape.

3. Lifecycle diagram (system view)

1. Construct

Browser fetches the worker script, spins a thread + global scope. Failures (e.g. bad URL / cross-origin policy) surface as error on the Worker.

2. Message

Either side postMessages. Receiver runs onmessage. Payload is a clone (unless you transfer — later lesson).

3. Work

Worker runs long tasks without blocking main-thread paint/input. Main thread only updates DOM from onmessage.

4. Error

Runtime errors in the worker fire error on the main Worker (message, filename, lineno). Also listen for messageerror when a message cannot be deserialized.

5. Shutdown

Main: terminate() — immediate kill. Worker: close() — discard queued tasks, stop the scope. SPA unmount → terminate (and drop references).

Action Who calls Use when
terminate() Main thread Component unmount, cancel job hard, abandon hung work (MDN: does not let the worker finish)
close() Inside worker Worker finished its purpose and can self-retire cleanly
Drop all refs Main thread After terminate (or natural end) so the handle can be GC’d

4. Design-review deal-breakers (preview)

5. Light hands-on — order the lifecycle

Drag is optional; click the steps in the order a healthy SPA client should use them. Immediate feedback.

Lifecycle order

Click steps in correct order (1 → 5). Wrong picks reset.

Progress: 0 / 5

6. Practice — design decisions

Equal-length options. Pick the best default for each review comment.

Scenario A

React route unmounts a page that owns a long-running CSV worker. What should cleanup do?

Scenario B

Main thread does obj.x = 1; worker.postMessage(obj); obj.x = 2; What does the worker see for x?

Scenario C

Photo app applies filters often. Team proposes new Worker on every button click.

Scenario D

Design review: “How do we know the worker script failed to load or threw?”

7. What to remember

Ask your teacher Fuzzy bits — module workers in Vite, Blob workers, correlating promises to messages, or a snippet from your app — ask in chat.

Primary source (read next)

MDN — Using Web Workers (dedicated section: spawn, messages, terminate, errors). Skim the WHATWG tutorial: creating & communicating for the normative short path.