Browser & Frontend Runtime
Web worker mechanics: dedicated, shared, and OffscreenCanvas
You know workers exist. But new Worker('script.js') spawns a dedicated thread that dies with the tab — so what do you use when five tabs need the same WebSocket connection, or when WebGL rendering should survive main-thread saturation?
Dedicated workers
new Worker('worker.js') spins up a fresh JS realm on its own OS thread, with its own event loop, its own global (DedicatedWorkerGlobalScope), and no access to document, window, or the DOM. It can use fetch, IndexedDB, WebSocket, OffscreenCanvas, timers, and importScripts/ES modules. Communication is exclusively postMessage in both directions.
A worker is tied to the page that created it: close the tab and the worker dies. Use it for anything CPU-bound that would otherwise blow the 50 ms task budget — JSON parsing of large payloads, image filters, cryptographic hashing, diffing, compression, syntax highlighting, physics.
The same-origin constraint. The worker script URL must be same-origin (or a blob URL). You cannot load a worker straight from a third-party CDN without proxying it through your own origin first. This is the same origin boundary that protects the rest of the browser’s security model.
Shared workers
new SharedWorker('s.js') creates a worker that is shared across all tabs of the same origin. Each tab connects through a MessagePort, and the worker lives as long as any tab uses it — useful for one shared WebSocket connection or a coherent cross-tab cache. Support is weaker than dedicated workers (absent in some browser configurations) and debugging is harder, because the worker’s lifetime is decoupled from any single tab.
Module workers
The { type: 'module' } option changes how a worker loads its dependencies:
- Classic worker — uses
importScripts()to load dependencies synchronously. - Module worker — uses ordinary static
import. This gives tree-shaking, top-levelawait, and the same module graph as the rest of the app.
Module workers are the modern default. Classic importScripts remains only for legacy code and cases that build the script name dynamically at runtime.
- Dedicated worker lifetime
- Tied to creating page
- Shared worker lifetime
- Last subscribing tab
- Module worker
- { type: 'module' } option
- Worker startup overhead
- 5–20 ms + script parse
- Max pool size (rule of thumb)
- hardwareConcurrency − 1
OffscreenCanvas: the one DOM carve-out
A worker cannot touch the DOM, but canvas is a special case — a canvas bitmap is not part of the DOM tree, it is just a pixel buffer. OffscreenCanvas is the canvas decoupled from a <canvas> element.
The pattern:
- Main thread calls
canvas.transferControlToOffscreen(), gets anOffscreenCanvas. - Transfers it to a worker through postMessage’s transfer list.
- Worker gets a 2D or WebGL context and renders entirely on its own thread.
- The browser reflects the result into the visible
<canvas>— with no main-thread involvement.
This makes smooth 60 fps WebGL visualisations, map rendering, and games possible even while the main thread is saturated.
A worker can also create a standalone new OffscreenCanvas(w, h) purely for off-screen rendering — texture generation, image processing — and return the result as a transferable ImageBitmap.
The exclusivity constraint. An OffscreenCanvas whose control was transferred cannot also be drawn from the main thread — ownership is exclusive, exactly like a transferred ArrayBuffer. If both threads need to draw, you need two canvases or a different architecture; the platform deliberately forbids two threads racing on one bitmap.
Five browser tabs from the same origin each need a single shared WebSocket connection managed in one place. Which worker type fits?
After `canvas.transferControlToOffscreen()`, the main thread tries to call `ctx.fillRect(0,0,100,100)`. What happens?
A worker that is shared across all tabs of the same origin — each tab connects through a separate port — is called what?
Why this works
Why does OffscreenCanvas exist if workers cannot touch the DOM? Canvas is special because its output is a flat bitmap — it has no DOM events, no CSS, no accessibility tree. The browser’s rendering pipeline reads the bitmap and composites it; nothing else in the DOM tree cares who wrote the pixels. This makes it possible to safely hand pixel-writing to any thread. Generalising this to all DOM would require locking the entire tree — which is exactly what the worker model avoids.
- 01What are the three worker variants and when do you use each?
- 02Why must a worker script be same-origin (or a blob URL)?
- 03After calling canvas.transferControlToOffscreen(), can the main thread still draw to that canvas?
A dedicated worker is a fresh JS realm, event loop, and OS thread tied to the page that created it — it dies when the page closes. A shared worker spans all tabs of an origin via MessagePort connections, surviving as long as any tab uses it. Module workers use ES imports instead of the legacy importScripts. OffscreenCanvas is the one way workers can render: transfer a canvas’s control to a worker and it draws 2D or WebGL on its own thread, with the browser compositing the result — but ownership is exclusive and transfer is permanent.
appears again in41
- Federation and lookahead: batching beyond DataLoadermiddle
- Senior GraphQL API: scheduling contract, tenant isolation, observabilitysenior
- Lock and single-flight: bounding concurrent rebuildsmiddle
- Stale-while-revalidate and CDN request coalescingmiddle
- Detecting stampedes and designing TTL for productionmiddle
- Metastable failure, fencing tokens, and production postmortemssenior
- What a relation is: tables, rows, keys, and constraintsjunior
- Constraints, keys, and Postgres data typesmiddle
- JSONB, arrays, and when a side table winsmiddle
- Schema integrity: deferral, versioning, and production failure modessenior
- Where data fetching happens — and why it decides LCPjunior
- React Server Components and Suspense streamingmiddle
- Senior internals: RSC payload, caching layers, and production failure modessenior
- The IP envelopejunior
- Reading the IP headermiddle
- What TLS does and why it existsjunior
- Key schedule, SNI, ALPN, and extensionssenior
- 0-RTT defenses, ECH, hybrid PQ, and production TLSsenior
- The twelve layers: one URL, seven actorsjunior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What is OpenTelemetry: API, SDK, Collector, OTLPjunior
- OTel signals, Semantic Conventions, and the OTLP wire formatmiddle
- The OTel Collector: receivers, processors, exporters, and deployment patternsmiddle
- Vendor neutrality, eBPF instrumentation, the Operator, and browser/serverless OTelsenior
- Operating the OTel Collector: reliability, version skew, failure modes, and governancesenior
- What is trace propagation and why broken propagation is worse than nonejunior
- traceparent and tracestate: the W3C header format in fullmiddle
- Baggage and async boundaries: carrying context across queues and callbacksmiddle
- Async context per language, service mesh, B3 migration, and securitysenior
- Production propagation failures, span links, and platform designsenior
- The debugging funnel: SLO → RED → trace → profilejunior
- OTel architecture: one SDK, four signals, one wire formatmiddle
- The incident loop: from pager to postmortem to preventionmiddle
- Scale, security, and the ROI of observable systemssenior
- At-most-once, at-least-once, exactly-once: the three delivery contractsjunior
- Consumer-side dedup: the cheapest path to exactly-once processingmiddle
- Exactly-once in production: impossibility proof, hybrid patterns, and real incidentssenior
- What OAuth is and why passwords are not the answerjunior
- Authorization code flow with PKCEmiddle
- Sender-constrained tokens: DPoP and mTLSsenior
- OAuth in production: audience attacks, observability, and real failuressenior