Web Workers 101: What They Are, How They Work, and Why They Exist

March 5, 20265 min read
service workerPWAofflinecachingweb workersprogressive web appsbackground sync

Why Web Workers First? 🤔

Before jumping into service workers, Kyle makes sure we understand web workers — because service workers are a special kind of web worker. Understanding the foundation makes everything that comes after click into place much faster.

The exercise setup is simple: spin up a local server (node server.js) on port 8049, open localhost:8049 in the browser, and you've got a basic site to work with. Nothing fancy — it doesn't do anything yet. That's the point.


What Is a Web Worker? 💡

A web worker is a JavaScript file that runs on a separate thread from your main web page thread.

Normally, JavaScript is single-threaded. If you run heavy processing code on the main thread, the entire page freezes — no scrolling, no clicking, nothing. Web workers solve this by offloading that heavy work to a background thread so the page stays responsive.

"If I have some heavy-lifting processing work to do and I need to do it front end, I can offload it to a web worker."

The classic demo for this (and what Kyle uses here) is recursively calculating Fibonacci numbers — intentionally unoptimized, intentionally slow, so we can actually feel the processing load. On Kyle's machine it starts getting painfully slow around Fibonacci(42) or Fibonacci(43). That's the point — it's a stand-in for any real CPU-intensive work.


What the Browser Actually Guarantees 📋

This is where I had to slow down and re-read, because the guarantees are narrower than you'd expect:

  1. At least one extra thread — spinning up a web worker guarantees it runs on a different thread from the main page. That's the only hard guarantee.
  2. Multiple workers ≠ multiple guaranteed threads — if the browser guaranteed a unique thread per worker, it'd be trivially easy to DoS someone's device. So it doesn't.

Three Types of Workers 🗂️

1. Dedicated Worker

  • Belongs to a single tab/page
  • Killed immediately when that tab closes
  • Most common, most widely supported
  • What Kyle uses in this course

2. Shared Worker

  • Shared across multiple tabs of the same site
  • Lives as long as at least one tab is open
  • Useful for syncing state across tabs (e.g., a cart icon updating across all open tabs of a shopping site)
  • Not reliably cross-browser — doesn't work on mobile devices, wasn't supported in some browsers for a long time
  • Don't build core functionality on it; treat it as progressive enhancement

3. Child Worker

  • A worker spun up by another worker
  • Works in some browsers/configurations, not others
  • Niche use case

"I've used shared workers maybe once or twice in my career. Dedicated workers are far more useful day to day."


Shared Worker Fallback — Q&A Note 🙋

Someone in the class asked: "Is there an automatic fallback from shared worker to dedicated worker?"

Kyle's answer: No automatic fallback. You have to feature-detect whether a shared worker is available and then manually spin up a dedicated worker instead.


Service Workers vs. Web Workers ⚡

Here's what makes service workers genuinely special compared to regular web workers:

Web WorkerService Worker
Tied to a tab?✅ Yes — dies when tab closes❌ No — survives beyond the tab
Lives in background?Only while tab is openYes, even after navigation
Lifecycle managed by?Your codeThe browser

Kyle's analogy:

You basically have this worker that is almost like a zombie. It's living outside of your tab.

A service worker can be spun up even when your page isn't open — but the browser manages its lifecycle tightly. It'll live for maybe 5–15 seconds while doing work, then shut down when idle. The security model keeps it scoped only to the pages it was registered for.


The Demo: What We're Building 🛠️

  1. Click Start → fires off a web worker that recursively generates Fibonacci numbers and sends each result to the page
  2. The page displays each number as it arrives
  3. Click Stop → kills the worker

One important nuance about why we kill instead of message to stop:

Worker communication is asynchronous event-based (via postMessage). If the worker is busy churning through a calculation, it can't process incoming messages — they just queue up. So you can't tell a busy worker to stop. You have to terminate it from the outside.


Creating the Worker 🛠️

The Two Files

Inside web/js/ there are exactly two JavaScript files:

  1. home.js — loaded by the web page. Handles button clicks, DOM updates, communication.
  2. worker.js — runs in the separate thread. Does the actual work.

Mental model to lock in: one file for the page, one file for the background thread.

Scoping the Worker Variable

let worker;

Declared at the top level so both startFibs and stopFibs can access it.

Spinning It Up

worker = new Worker('/js/worker.js');

One line. Pass the URL of the JS file, browser handles the rest.

Two things to note:

  1. Browser support — if you have service worker support, you have web worker support. Feature-detect with typeof Worker for edge cases.
  2. No DOM access — the worker can't touch the page's DOM, but it does have network access and can make fetch/ping requests independently.

First Message Test

In worker.js:

self.postMessage('hello from the web worker');

In home.js:

worker.addEventListener('message', function(evt) { console.log(evt.data); });

Reload → click Start → console shows: hello from the web worker

Always terminate the worker when done testing. Leaving it running drains battery and spins up your fan fast.