# Web Workers vs Shared Workers vs Service Workers: A Practical Guide to Choosing the Right One

*Reading time: ~13 minutes · Level: Intermediate JavaScript · Updated: October 2026*

A page can freeze even though it uses a Service Worker. Two tabs can disagree even though each has a Web Worker. And an offline app can fail even though its Service Worker registered successfully.

The problem is treating *worker* as one job description. Each type has a different **owner, communication model, and lifetime**.

> **💡 The mental model:** A dedicated worker is a **private assistant** for one caller. A Shared Worker is a **coordinator** for connected tabs. A Service Worker is an **event-driven request handler** for clients within its scope.

![](https://cdn.hashnode.com/uploads/covers/69328640f726ffe2419b1324/3c5dcc10-9f70-4f4e-8daf-548c587f9a8f.png align="center")

We will compare their communication patterns, lifecycles, and real-world use cases, then build a small example of each.

## What all three have in common

Workers execute JavaScript outside the page’s main execution thread. They have their own global context, generally referenced as `self`, and **cannot directly manipulate the page’s DOM**. A worker that needs to change a button or display a result sends information to the page; the page performs the DOM update. [MDN: Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) · [MDN: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)

That separation is useful, but it is not free. Messages generally use **structured cloning**, which can cost time and memory for large payloads. Moving a small calculation to a worker might add overhead without improving the experience; moving a substantial calculation can keep the main thread available for interactions. [MDN: Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#transferring_data_to_and_from_workers_further_details) · [web.dev: Web worker overview](https://web.dev/learn/performance/web-worker-overview)

## The comparison at a glance

| Question | Dedicated Web Worker | Shared Worker | Service Worker |
| --- | --- | --- | --- |
| **Primary job** | Compute for one caller | Coordinate connected contexts | Handle events and controlled requests |
| **Start with** | `new Worker(url)` | `new SharedWorker(url)` | `navigator.serviceWorker.register(url)` |
| **Typical connection** | Creator ↔ worker | Each client ↔ its `MessagePort` | Controlled client → `fetch` event |
| **Multiple tabs share one instance?** | ❌ Not by default | ✅ Eligible same-origin tabs can | ✅ One registration can serve multiple scoped clients |
| **Intercept page requests?** | ❌ | ❌ | ✅ For controlled clients |
| **Direct DOM access?** | ❌ | ❌ | ❌ |
| **State survives all tabs closing?** | ❌ | ❌ Do not rely on it | ❌ Global variables are not durable |
| **Typical use** | Image processing, parsing, simulation | One live connection feeding several tabs | Offline fallback, caching, push handling |

A Service Worker **does not automatically intercept every request from an origin**. Whether it handles a request depends on client control and scope; its `fetch` handler can also leave a request untouched. That distinction matters when debugging a first install. [MDN: Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) · [MDN: Fetch event](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch_event)

## 1\. Dedicated Web Workers: protect one page’s responsiveness

Suppose a page calculates prime numbers while a user continues clicking and scrolling. A long synchronous calculation on the main thread delays those interactions. A dedicated worker runs the calculation separately and returns the answer in a message. [web.dev: Web worker overview](https://web.dev/learn/performance/web-worker-overview)

![](https://cdn.hashnode.com/uploads/covers/69328640f726ffe2419b1324/9930cace-a989-4e62-ae9b-406f477906b3.png align="center")

### Example: calculate primes in a dedicated worker

Serve these files from a local development server. The page owns the UI; the worker owns the computation.

```html
<!-- index.html -->
<input id="limit" type="number" min="2" value="2000000" />
<button id="calculate">Count primes</button>
<p id="result" role="status"></p>

<script src="./main.js"></script>
```

```js
// main.js — page thread
const input = document.querySelector("#limit");
const button = document.querySelector("#calculate");
const result = document.querySelector("#result");
const worker = new Worker("./prime-worker.js");

button.addEventListener("click", () => {
  const limit = Number(input.value);

  if (!Number.isSafeInteger(limit) || limit < 2) {
    result.textContent = "Enter an integer of at least 2.";
    return;
  }

  button.disabled = true;
  result.textContent = "Calculating…";
  worker.postMessage({ limit });
});

worker.addEventListener("message", ({ data }) => {
  result.textContent = `Found ${data.count} primes up to ${data.limit}.`;
  button.disabled = false;
});

worker.addEventListener("error", (event) => {
  result.textContent = `Worker failed: ${event.message}`;
  button.disabled = false;
});
```

```js
// prime-worker.js — dedicated worker
self.addEventListener("message", ({ data }) => {
  const { limit } = data;
  let count = 0;

  for (let candidate = 2; candidate <= limit; candidate++) {
    let isPrime = true;

    for (let divisor = 2; divisor * divisor <= candidate; divisor++) {
      if (candidate % divisor === 0) {
        isPrime = false;
        break;
      }
    }

    if (isPrime) count++;
  }

  self.postMessage({ limit, count });
});
```

The page calls `worker.postMessage()`. The worker receives a `message` event and replies with `self.postMessage()`. To stop an unneeded dedicated worker immediately, call `worker.terminate()`. [MDN: Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#dedicated_workers)

### When one worker handles several jobs

Raw messages are easy for one request, but what happens when several requests are in flight? Give each request an **ID** and return that ID with its result. Then the page can match responses to the right Promise. This is a messaging pattern, not a different worker type. [MDN: Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#transferring_data_to_and_from_workers_further_details)

```js
// Page-side sketch: correlate concurrent requests.
let nextId = 0;
const pending = new Map();

worker.addEventListener("message", ({ data }) => {
  const request = pending.get(data.id);
  if (!request) return;

  pending.delete(data.id);
  data.error
    ? request.reject(new Error(data.error))
    : request.resolve(data.result);
});

function requestWorker(type, payload) {
  const id = ++nextId;

  return new Promise((resolve, reject) => {
    pending.set(id, { resolve, reject });

    try {
      worker.postMessage({ id, type, payload });
    } catch (error) {
      pending.delete(id);
      reject(error);
    }
  });
}
```

The matching worker must reply with `{ id, result }` or `{ id, error }`. In a real wrapper, also reject pending requests on worker failure or termination, and consider timeouts so a lost reply cannot leave a Promise pending forever.

### Large payloads: copy or transfer?

For ordinary messages, structured cloning produces data in the receiving context. For a large `ArrayBuffer`, **transfer ownership** when the sender no longer needs it:

```js
// Page thread
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ type: "process", buffer }, [buffer]);

// The sender no longer owns the buffer after transfer.
console.log(buffer.byteLength); // 0
```

This avoids copying the buffer’s underlying data, but the page cannot keep using the transferred buffer unless the worker transfers it back. `SharedArrayBuffer` is a different mechanism: it enables actual shared memory and brings additional security and synchronization requirements. [MDN: Transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) · [MDN: Using Web Workers—Sharing data](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#sharing_data)

**Use a dedicated worker when:** substantial work belongs to one caller and main-thread responsiveness matters.

## 2\. Shared Workers: coordinate several open tabs

Imagine a dashboard open in three tabs. If each tab opens its own WebSocket, you may create duplicate connections and process the same updates three times. A Shared Worker offers a place to manage **one connection for the currently connected tabs** and fan out its messages. That is an architectural option—not a promise of “one connection per user” across devices, browser profiles, or every possible browsing context. [MDN: SharedWorker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)

![](https://cdn.hashnode.com/uploads/covers/69328640f726ffe2419b1324/423d7416-93b8-4422-b3ba-2c914ba9ed89.png align="center")

### Example: live in-memory count across tabs

Open this page in two tabs of the **same origin**:

```html
<!-- counter.html -->
<button id="increment">Increment</button>
<p id="count" role="status">Connecting…</p>
<script src="./counter.js"></script>
```

```js
// counter.js — runs separately in each tab
const output = document.querySelector("#count");
const button = document.querySelector("#increment");

if ("SharedWorker" in window) {
  const worker = new SharedWorker("./counter-worker.js");
  const port = worker.port;

  port.addEventListener("message", ({ data }) => {
    if (data.type === "count") {
      output.textContent = `Shared count: ${data.value}`;
    }
  });

  port.start();
  button.addEventListener("click", () => {
    port.postMessage({ type: "increment" });
  });
} else {
  output.textContent = "Shared Workers are unavailable.";
  button.disabled = true;
}
```

```js
// counter-worker.js — shared by connected tabs
let count = 0;
const ports = new Set();

self.addEventListener("connect", (event) => {
  const port = event.ports[0];
  ports.add(port);

  port.addEventListener("message", ({ data }) => {
    if (data.type === "disconnect") {
      ports.delete(port);
      port.close();
      return;
    }

    if (data.type !== "increment") return;

    count++;

    for (const client of ports) {
      client.postMessage({ type: "count", value: count });
    }
  });

  port.start();
  port.postMessage({ type: "count", value: count });
});
```

The tabs use `worker.port.postMessage()`, not `worker.postMessage()`. When listeners are registered with `addEventListener("message", ...)`, start the port with `port.start()`; assigning `port.onmessage` starts it implicitly. [MDN: Using Web Workers—Shared workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#shared_workers)

This example keeps a set of ports to demonstrate broadcasting. In a production app, send the worker’s `disconnect` message and close the page-side port when appropriate, and handle reconnection after navigation or page restoration. **Do not treat** `count` **as durable:** Shared Workers normally shut down when no contexts reference them. [MDN: Shared worker lifetime](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#shared_worker_lifetime)

### Do you actually need a Shared Worker?

If your requirement is merely **“tell my other tabs that a setting changed,”** a [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) may be simpler. It broadcasts messages among eligible contexts but does not, by itself, create one central process that owns a WebSocket or in-memory state. Browser storage partitioning can also affect which contexts communicate. [MDN: Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)

As of October 2026, MDN labels `SharedWorker` **Baseline 2026, newly available** across current major browser versions, while warning that older browsers or particular features may differ. Feature-detect it if your audience includes older devices. [MDN: SharedWorker compatibility](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)

> **💡 Debugging tip:** Worker logs may appear in a worker-specific inspector rather than the page console. MDN points to `chrome://inspect/#workers` for Chrome and `about:debugging#workers` for Firefox. [MDN: SharedWorker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker#example)

**Use a Shared Worker when:** multiple active, eligible contexts need one live coordinator—not merely because they need to exchange an occasional message.

## 3\. Service Workers: respond to requests and browser events

A Service Worker is registered for an origin and path scope. Once active and controlling a client, it can receive `fetch` events and choose whether to return a network response, a cached response, or a generated one. It also participates in supported events such as push and background sync. Unlike a Shared Worker, it is **not a continuously running coordinator**: the browser can stop it between events and restart it later. [MDN: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) · [MDN: ServiceWorkerGlobalScope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope)

![](https://cdn.hashnode.com/uploads/covers/69328640f726ffe2419b1324/88424b8a-2990-4a15-84b0-ae6ae014b313.png align="center")

### Example: an offline fallback page

For this example, serve `/index.html`, `/app.js`, `/styles.css`, `/offline.html`, and `/sw.js` at the root of the same origin. The named files must exist. Use HTTPS, or a browser-trusted `localhost` development origin. [MDN: Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers)

```js
// app.js — page thread
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js", { scope: "/" })
    .catch((error) => {
      console.error("Service worker registration failed:", error);
    });
}
```

```js
// sw.js — service worker
const CACHE_NAME = "offline-demo-v1";
const PRECACHE_URLS = ["/offline.html", "/app.js", "/styles.css"];

self.addEventListener("install", (event) => {
  // A failed precache rejects installation.
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) =>
      cache.addAll(PRECACHE_URLS)
    ),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter(
            (name) =>
              name.startsWith("offline-demo-") &&
              name !== CACHE_NAME,
          )
          .map((name) => caches.delete(name)),
      ),
    ),
  );
});

self.addEventListener("fetch", (event) => {
  const request = event.request;

  // Handle only same-origin page navigations in this small example.
  if (
    request.mode !== "navigate" ||
    new URL(request.url).origin !== self.location.origin
  ) {
    return;
  }

  event.respondWith(
    (async () => {
      try {
        return await fetch(request);
      } catch {
        return (
          (await caches.match("/offline.html")) ||
          new Response("You are offline.", {
            status: 503,
            headers: { "Content-Type": "text/plain" },
          })
        );
      }
    })(),
  );
});
```

This is a **network-first navigation with a generic offline fallback**. It does not silently cache every page. That narrower policy matters: blindly storing account pages or API responses can create privacy and stale-data problems. The cache clean-up also deletes only this example’s cache names rather than every cache on the origin. [MDN: Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching)

### Choose a cache strategy per resource

**Your caching strategy is a product decision, not a default for every route.** Choose it according to how fresh each resource must be and what should happen offline. [MDN: Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching)

| Strategy | Response path | Potential fit | Main risk |
| --- | --- | --- | --- |
| **Cache first** | Cached response → network on miss | Versioned static assets | Serving stale assets if versions are mishandled |
| **Network first** | Network → cached fallback | Content where freshness matters | Slow responses on poor connections |
| **Stale while revalidate** | Cached response now; refresh for later | Content that tolerates temporary staleness | User sees old content until a later request |
| **Cache only** | Cached response or failure | Deliberately precached resources | A missing cache entry breaks the request |
| **Network only** | Always request network | Resources you intentionally do not cache | No offline response |

For example, a public article and a bank balance should not inherit the same caching rule merely because both are fetched with `GET`.

### Why the first load—and updates—behave differently

A newly registered Service Worker downloads, **installs**, then **activates**. The page that initiated registration is usually **not controlled on that first load**; reload or navigate to test the ordinary controlled-page behavior. `clients.claim()` can adopt eligible existing clients when appropriate. [MDN: Using Service Workers—Initial installation](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#initial_installation)

When an updated worker installs, it can remain **waiting** while the previous version controls open pages. `skipWaiting()` requests earlier activation, but can make an existing page interact with a different worker version. Do not add `skipWaiting()` and `clients.claim()` reflexively to every example: decide how your app handles version transitions first. [web.dev: The service worker lifecycle](https://web.dev/articles/service-worker-lifecycle)

### What about offline submissions and push?

Service Workers can also participate in offline submissions and push notifications, with important qualifications:

*   **Background Sync** can ask a Service Worker to retry queued work when connectivity is suitable, but the API has **limited browser availability**. Persist the queue—for example, in IndexedDB—and design a fallback such as retrying when the app opens. Do not keep unsent form data only in a Service Worker global variable. [MDN: Background Synchronization API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API) · [MDN: ServiceWorkerGlobalScope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope)
    
*   **Push notifications** are browser-managed events, not evidence that your Service Worker stays awake continuously. Check platform support and permission requirements before promising the same notification behavior to every user. [MDN: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) · [MDN: Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
    

**Use a Service Worker when:** you need deliberate request handling, offline behavior, or supported event-driven capabilities. Do not use it as a home for an unbounded computation loop.

## Five mistakes worth avoiding

1.  **“I used a worker, so my UI cannot freeze.”** The worker cannot prevent unrelated main-thread work from blocking the page; measure the actual bottleneck. [web.dev](https://web.dev/learn/performance/web-worker-overview)
    
2.  **“Shared Worker means persistent shared storage.”** Its in-memory state can disappear when it is no longer needed. Persist important data elsewhere. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#shared_worker_lifetime)
    
3.  **“My Service Worker registered, so it handles this page already.”** Registration, activation, and **client control** are distinct. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#initial_installation)
    
4.  **“A failed HTTP response will enter my** `fetch()` **catch block.”** An HTTP error such as `404` is still a response; check `response.ok` if your policy needs to treat it as failure. [MDN: ServiceWorkerGlobalScope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope)
    
5.  **“Caching all GET requests is safe.”** Choose routes and expiration behavior explicitly, especially for personalized content. [MDN: Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching)
    

## The decision rule

Ask **who owns the work**:

*   **One caller, expensive computation?** Dedicated Web Worker.
    
*   **Several open tabs, one live coordinator?** Shared Worker.
    
*   **Several tabs, only simple notifications between them?** Consider `BroadcastChannel` before introducing a coordinator.
    
*   **Controlled requests, offline responses, or supported background events?** Service Worker.
    
*   **Data that must survive tab or worker termination?** Add persistent storage; none of these worker types provides that guarantee on its own.
    

You can combine them. An offline dashboard might use a **Service Worker** for navigation and assets, a **Shared Worker** for one live update stream across its open tabs, and a **dedicated worker** in a tab that performs expensive analysis. Each has one clear responsibility. [MDN: Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) · [MDN: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)

## Conclusion

The difference is not how “background” a worker sounds. It is **who can reach it, what events it receives, and whether you can depend on its lifetime**.

**Try this next:** Run the dedicated-worker example and interact with the page during calculation. Open the Shared Worker example in two tabs. Then install the Service Worker, reload, and test a navigation offline. Those three experiments reveal the architectural differences faster than another comparison table.

## Further reading

*   [MDN: Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)
    
*   [MDN: SharedWorker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker)
    
*   [MDN: Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers)
    
*   [MDN: Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
    
*   [web.dev: The service worker lifecycle](https://web.dev/articles/service-worker-lifecycle)
