Lesson 0004 · ~12 minutes
One skill: run a design-review risk scan — what a dedicated worker can do, what it can’t, and which integration edges kill the plan before code ships.
You can reject a bad worker proposal in 60 seconds (DOM coupling, wrong tool, tooling/lifecycle missing) and green-light a good one with eyes open.
MDN’s core rule still drives architecture: you can run almost any code
in a worker, with important exceptions — notably
you cannot directly manipulate the DOM or treat the
page window as your global
(Web Workers API).
System design split that works:
That is the same “UI thread vs everything else” split native platforms push — web.dev’s OMT guide leans on it hard.
Authoritative inventory: Functions and classes available to workers. High-signal FE facts:
| Usually yes | Usually no / different |
|---|---|
fetch, timers, console,
IndexedDB, WebSockets, structured clone helpers,
many binary/media helpers
|
DOM APIs, page window,
localStorage/sessionStorage (prefer
IDB), React/Vue render
|
Classic: importScripts; module workers:
import
|
Assuming every main-thread API exists — always check the list |
“What does this code touch that is DOM or storage-API specific?” If the answer is “half the module,” either split a pure core or keep it on main.
new Worker("worker.js") is easy in demos and fragile in
apps. Modern guidance (MDN + webpack/Vite docs): resolve the worker as a
module asset:
// Portable pattern (webpack 5+, Vite, etc.)
const worker = new Worker(
new URL("./worker.js", import.meta.url),
{ type: "module" } // if the worker is ESM
);
// Vite also supports:
// import Worker from "./worker.js?worker"
Risk: wrong relative path, CDN/publicPath mismatches, classic vs module mismatch. Budget “make the worker load in prod” as a real task.
Worker scripts are subject to origin rules. Cross-origin worker scripts
are a common footgun; modern browsers surface failure via the worker
error event rather than a helpful constructor throw (see
MDN Worker). Prefer shipping the worker with your app origin/bundle.
From lesson 2: create → message → terminate on unmount.
Deal-breaker is organizational: if no component “owns” the worker, you
leak threads across SPA navigations. Spec note: workers are relatively
heavy — reuse, don’t spawn casually
(WHATWG).
Multi-job workers without type + correlation
id become racey soup. Missing onerror /
messageerror means silent production failure. This is
integration, not trivia.
Offline cache, push, request interception →
service worker. Multi-tab shared session bus → maybe
shared worker (later) or
BroadcastChannel. Don’t force dedicated workers into those
shapes.
Work already fits budgets on target devices — measure first; worker overhead can dominate.
The bottleneck is layout/paint/React commit, not pure JS — workers can’t paint your tree for you.
Chunking on main (lesson 1 spectrum) already keeps input responsive — simpler ops story.
Protocol + bundler + debug + clone design exceeds the jank risk you actually have.
Equal-length options. Design-review mode.
Proposal: move the React list virtualizer (measures DOM nodes, writes styles) fully into a dedicated worker.
Proposal: parse large JSON reports in a long-lived module worker; main only renders table rows from results; terminate on leave.
Proposal: “Add a dedicated worker so the SPA works offline and caches the app shell.”
Proposal: for email format checks (~0.1ms), spawn a worker on every keystroke and post the full form state object.
Full table: integration checklist.
fetch, IDB, …); verify the list.MDN — Functions and classes available to workers (capability inventory). Optional: your bundler’s worker docs (webpack / Vite).