HTML Tutorial 0/139 lessons ~6 min read Lesson 58

    HTML Web Workers

    html web workers web workers keep main-thread inp healthy by isolating parse, search, and crypto web workers run javascript

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Web Workers run JavaScript on background threads — no DOM access, message-passing only via postMessage. Staff engineers offload CSV parsing, search indexing, and crypto to workers like Google Docs and Figma — keeping main thread INP under budget. Know transferables, worker termination on SPA navigation, and SharedWorker/Service Worker distinct roles.

    Business problem

    Business pressure: Main-thread jank during heavy compute loses users — spreadsheet recalc, PDF parse, image resize on upload. Slack search built inverted index in worker; skipping worker crashed tab on 10MB paste. Mobile CPUs punish sync parse on main thread with ANR-like freezes.

    • Conversion: Checkout freeze during tax calc — worker + skeleton UI preserves completion rate.
    • Compliance: Crypto operations in worker don't reduce PCI scope if card data still hits main — but keeps UI responsive during tokenization.
    • SEO: Workers irrelevant to crawl — SSR must output indexable HTML without waiting for worker results for critical content.

    Why this feature exists

    Platform motivation: JavaScript single-threaded model blocked UI during long tasks. Workers (2009) added parallel compute without shared-memory races (until SharedArrayBuffer returned with COOP/COEP). Alternative to "just use WASM on main thread."

    • History: Dedicated Worker most common; SharedWorker rare (Safari gaps); Service Worker for network intercept offline.
    • Alternative rejected: setTimeout chunking on main — still contends with input; inferior to true parallel.
    • Modern role: Module workers (type: module), OffscreenCanvas in worker for image decode — Chrome-first patterns spreading.

    Browser internals

    Inside the engine: new Worker(url) spawns parallel V8 isolate — separate event loop, no document/window. postMessage structured-clones data (expensive for large objects) or transfers ArrayBuffer ownership zero-copy. Worker script subject to same-origin and CSP worker-src. terminate() kills immediately — leak if forgotten on SPA route change.

    • Parser: Worker script loaded async — not parser-blocking unlike sync script in HTML.
    • DOM: No access — UI updates must postMessage back to main.
    • Lifecycle: Garbage collected when terminated + no references — browsers cap worker count per origin (~20).
    javascript
    // main.js
    const worker = new Worker('/parse-worker.js', { type: 'module' });
    worker.postMessage({ csv: hugeString });
    worker.onmessage = e => renderTable(e.data.rows);
    worker.onerror = e => console.error(e.message);
    // parse-worker.js
    self.onmessage = e => {
    const rows = parseCsv(e.data.csv);
    self.postMessage({ rows });
    };

    Rendering workflow

    Rendering path: Workers don't paint — main thread receives results and updates DOM (often triggering layout). Batch DOM updates from worker results in DocumentFragment or virtual DOM commit — Google Sheets batches cell updates per animation frame.

    • Critical path: Don't await worker before first paint — show skeleton, stream partial results.
    • Layout: Injecting 10k table rows from worker — use virtualization on main.
    • Paint: OffscreenCanvas in worker — transfer bitmap to main via transferToImageBitmap.

    Feature deep dive

    Worker types: Dedicated (one page), Shared (multiple tabs — rare), Service (network proxy — PWA offline). Module workers support import/export. Inline workers via Blob URL — CSP may block. Comlink library simplifies RPC-style API.

    • When to use: CPU >50ms tasks — parsing, compression, hashing, sort 100k items.
    • When not: Tiny tasks — postMessage overhead exceeds savings.
    • Transferables: ArrayBuffer, ImageBitmap, OffscreenCanvas — zero-copy handoff.
    html
    <!-- HTML loads main script only; worker spawned from module -->
    <script type="module">
    import { initSearch } from '/search-main.js';
    initSearch(); // creates worker internally
    </script>

    Accessibility analysis

    A11y architecture: Workers compute — main thread must expose progress to AT via aria-busy and aria-live. Long task without feedback feels broken to screen reader users — announce "Processing 50%." Don't block keyboard while worker runs — main thread still handles input if not saturated.

    • Screen readers: aria-live polite on status region during worker job.
    • Keyboard: Cancel button must terminate worker — AbortSignal pattern via message.
    • WCAG: 2.2.6 Timeouts — long worker task needs progress + extend option if session-bound.

    SEO impact

    SEO architecture: Crawlers don't run your worker pipeline for ranking-critical content. SSR/SSG must emit full HTML for product lists — client worker enhances search filter only. Dynamic rendering fallback if worker required for content (anti-pattern).

    • Crawl: Initial HTML complete without worker — progressive filter OK.
    • Rich results: Schema in SSR HTML — worker-filtered view is UX layer.
    • CWV: Worker improves INP — positive ranking signal via field data.

    Security considerations

    Security boundary: Worker script must same-origin or CORS — malicious worker URL is XSS equivalent. postMessage accepts any structured clone — validate message shape before processing on main (prototype pollution). No DOM doesn't mean no fetch — worker can exfiltrate if CSP connect-src loose.

    • CSP: worker-src 'self' — blocks injected Blob worker from XSS string.
    • Data: Don't postMessage secrets to worker if untrusted code can register listener in extension context — edge case but audit extensions.
    • SAB: SharedArrayBuffer requires cross-origin isolation headers — Spectre mitigation.

    Performance impact

    Performance: Long Task API flags main >50ms — workers eliminate many. postMessage clone 10MB JSON costs ms — use transferables or split chunks. Pool workers for parallel map-reduce — don't spawn 100 workers on mobile.

    • INP: Primary win — keep input handlers on main responsive during background parse.
    • Memory: Duplicate data in worker + main until transfer — plan lifecycle.
    • Startup: Worker cold start ~ms — reuse pool for repeated tasks (Figma pattern).

    Real production example

    Google Docs spell-check pattern: Typing on main; dictionary lookup and suggestion compute in worker; results posted back debounced 100ms; worker terminated on doc close. Figma: geometry tessellation in worker, render commands to canvas on main.

    • Pool: navigator.hardwareConcurrency - 1 workers max on desktop.
    • Fallback: Main thread parse with requestIdleCallback on unsupported browsers — rare.
    • Monitoring: Sentry breadcrumbs for worker errors — separate from main stack.
    javascript
    class WorkerPool {
    constructor(url, size = 2) {
    this.workers = Array.from({ length: size }, () => new Worker(url));
    this.queue = [];
    }
    run(data) {
    return new Promise((resolve, reject) => {
    const w = this.workers.find(w => !w.busy) ?? this.workers[0];
    w.busy = true;
    w.onmessage = e => { w.busy = false; resolve(e.data); };
    w.onerror = reject;
    w.postMessage(data);
    });
    }
    }

    Enterprise usage

    Enterprise: CSP worker-src whitelists bundled worker chunks in webpack/vite — no dynamic user worker URLs. Air-gapped builds inline workers as base64 — increases bundle but satisfies policy. Excel Online workers for formula recalc — fallback message if workers disabled.

    • Build: new URL('./worker.js', import.meta.url) — Vite worker pattern.
    • Testing: Jest worker mock or run integration in real browser only.
    • MDM: Some corporate policies disable workers — detect and degrade.

    Common production failures

    What breaks in prod: Worker leak on SPA navigation (10 workers idle), postMessage megabyte clone freeze, Safari module worker edge cases, forgot terminate on unmount — tab memory GB.

    • Incident: CSV import worker duplicated 500MB array main+worker — mobile OOM crash.
    • Perf: Spawning new worker per keystroke — pool fixed 200ms lag.
    • CSP: worker-src missing — workers silently fail prod only.

    Architecture review questions

    • Is worker terminated on component unmount / route change?
    • Are large payloads transferred not cloned?
    • Is there main-thread fallback if Worker undefined?
    • Does UI show progress via aria-busy/live during compute?
    • Is worker-src in CSP configured?
    • Is worker pool sized to hardwareConcurrency?

    Hands-on project

    Project: CSV upload: main UI + worker parse + progress aria-live + transferable buffers + worker pool + terminate on route leave.

    • Deliverable: Module worker via Vite; INP stays <200ms during 1MB parse.
    • Verify: DevTools memory stable after 10 navigations; axe busy states.
    • Stretch: OffscreenCanvas thumbnail in worker.

    Interview questions

    Worker vs Service Worker vs SharedWorker?(Advanced)

    Dedicated: page background compute. Service Worker: network intercept, push, offline cache — persists beyond page. SharedWorker: shared between tabs — rare, poor Safari support. Different problems.

    Follow-up: Can Service Worker access DOM?

    postMessage performance with large data?(Advanced)

    Structured clone copies — expensive. Use transferable ArrayBuffer to move ownership. Chunk stream processing. Comlink abstracts but same underlying cost.

    Follow-up: SharedArrayBuffer requirements?

    How would you refactor a 500ms main-thread sort?(Advanced)

    Move sort to worker, postMessage indices or transferable TypedArray, main applies reorder to virtualized list, show skeleton during compute, terminate worker on cancel.

    Follow-up: When is setTimeout chunking enough?

    Try it yourself

    Edit the HTML, CSS, or JS panels — the preview updates as you type.

    Try it yourself

    Preview

    Summary

    Web Workers keep main-thread INP healthy by isolating parse, search, and crypto workloads — with transferable buffers, worker pools, lifecycle termination, CSP worker-src, and accessible progress feedback on the main UI thread.

    Ready to mark this lesson complete?Track your journey across the entire course.