Skip to content

Shared Workers

This lesson explains Shared Workers with clear examples, use cases, and best practices so you can use HTML5 APIs confidently in real web applications.

HTML5 Shared Workers API Overview

The HTML5 Shared Workers API allows multiple browser contexts from the same origin to share one background worker. Unlike a Dedicated Worker, which belongs to one page, a Shared Worker can be used by multiple tabs, windows, iframes, or scripts at the same time.

Shared Workers are useful for sharing state, coordinating messages, managing a single WebSocket connection, caching data in memory, handling background calculations, syncing same-origin tabs, and reducing duplicate work across multiple open pages.

Feature Description
API Name Shared Workers API
Main Constructor new SharedWorker()
Worker Script Context SharedWorkerGlobalScope
Communication Object MessagePort
Main Event connect
Best Use Shared state, cross-tab coordination, single background process.

Basic Shared Worker Page Example

// main.js

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

  worker.port.start();

  worker.port.postMessage(
    {
      type: "PING",
      message: "Hello from page"
    }
  );

  worker.port.onmessage = (event) => {
    console.log(
      "Message from shared worker:",
      event.data
    );
  };
} else {
  console.log("Shared Workers are not supported.");
}

A page connects to a Shared Worker using new SharedWorker(). Communication happens through worker.port.

Basic Shared Worker Script

// shared-worker.js

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

  port.start();

  port.onmessage = (messageEvent) => {
    if (messageEvent.data.type === "PING") {
      port.postMessage(
        {
          type: "PONG",
          message: "Hello from shared worker"
        }
      );
    }
  }
});

The Shared Worker receives a connect event each time a new page, tab, iframe, or window connects to it.

How Shared Workers Work

Step Description Example Syntax
Create Worker Page creates or connects to a shared worker. new SharedWorker("/shared-worker.js")
Open Port Start communication through the worker port. worker.port.start()
Send Message Page sends data to the worker. worker.port.postMessage(data)
Connect Event Worker receives connection from each page. self.addEventListener("connect", callback)
Store Port Worker can keep each connected page port. ports.push(port)
Broadcast Reply Worker sends messages to one or more connected pages. port.postMessage(message)

Shared Worker Methods and Properties

Method / Property Description Example Syntax
new SharedWorker() Creates or connects to a shared worker. new SharedWorker("/shared-worker.js")
worker.port MessagePort used to communicate with the worker. worker.port.postMessage(data)
port.start() Starts receiving messages on a MessagePort. worker.port.start()
port.postMessage() Sends a message through the port. port.postMessage(message)
port.onmessage Handles messages from the connected context. port.onmessage = callback
connect Worker event fired when a context connects. self.addEventListener("connect", callback)

Broadcast Message to All Connected Tabs

// shared-worker.js

const ports =
  [];

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

  ports.push(port);

  port.start();

  port.onmessage = (messageEvent) => {
    ports.forEach((connectedPort) => {
      connectedPort.postMessage(
        {
          type: "BROADCAST",
          message: messageEvent.data.message
        }
      );
    });
  }
});

This example keeps a list of connected ports and sends a message to every connected tab or window.

Shared State Across Tabs

// shared-worker.js

let counter = 0;
const ports = [];

function broadcastCounter() {
  ports.forEach((port) => {
    port.postMessage(
      {
        type: "COUNTER_UPDATED",
        value: counter
      }
    );
  });
}

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

  ports.push(port);
  port.start();

  port.postMessage(
    {
      type: "COUNTER_UPDATED",
      value: counter
    }
  );

  port.onmessage = (messageEvent) => {
    if (messageEvent.data.type === "INCREMENT") {
      counter += 1;
      broadcastCounter();
    }
  }
});
// main.js

const worker =
  new SharedWorker("/shared-worker.js");

worker.port.start();

worker.port.onmessage = (event) => {
  if (event.data.type === "COUNTER_UPDATED") {
    document.querySelector("#count").textContent =
      event.data.value;
  }
}

document
  .querySelector("#increment")
  .addEventListener("click", () => {
    worker.port.postMessage(
      {
        type: "INCREMENT"
      }
    );
  });

This pattern allows multiple tabs to share one in-memory counter while keeping the UI in each tab synchronized.

Single WebSocket Connection Example

// shared-worker.js

const ports = [];

const socket =
  new WebSocket("wss://example.com/live");

socket.addEventListener("message", (event) => {
  ports.forEach((port) => {
    port.postMessage(
      {
        type: "SOCKET_MESSAGE",
        data: event.data
      }
    );
  });
});

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

  ports.push(port);
  port.start();

  port.onmessage = (messageEvent) => {
    if (messageEvent.data.type === "SEND_SOCKET") {
      socket.send(
        messageEvent.data.payload
      );
    }
  }
});

A Shared Worker can maintain one WebSocket connection and share updates across multiple tabs, reducing duplicate server connections.

Shared In-Memory Cache Example

// shared-worker.js

const cache =
  new Map();

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

  port.start();

  port.onmessage = async (messageEvent) => {
    const request =
      messageEvent.data;

    if (request.type === "GET_PRODUCT") {
      if (cache.has(request.id)) {
        port.postMessage(
          {
            type: "PRODUCT_RESULT",
            product: cache.get(request.id),
            source: "cache"
          }
        );

        return;
      }

      const response =
        await fetch("/api/products/" + request.id);

      const product =
        await response.json();

      cache.set(request.id, product);

      port.postMessage(
        {
          type: "PRODUCT_RESULT",
          product: product,
          source: "network"
        }
      );
    }
  }
});

Shared Workers can cache API data in memory and reuse it across tabs during the worker lifetime.

Shared Worker Error Handling

// main.js

const worker =
  new SharedWorker("/shared-worker.js");

worker.onerror = (error) => {
  console.error(
    "Shared Worker error:",
    error.message
  );
}

worker.port.onmessageerror = (error) => {
  console.error(
    "Message could not be cloned:",
    error
  );
}

Handle worker errors and message serialization issues to make shared background features more reliable.

Structured Message Contract Example

worker.port.postMessage(
  {
    type: "USER_STATUS_CHANGED",
    version: 1,
    payload: {
      userId: 101,
      status: "online"
    }
  }
);

Using a clear message structure with type, version, and payload makes communication easier to validate, document, and maintain.

Dedicated Worker vs Shared Worker

Feature Dedicated Worker Shared Worker
Owned By One page or script. Multiple same-origin pages, tabs, or iframes.
Constructor new Worker() new SharedWorker()
Communication Direct postMessage(). Uses MessagePort.
Best Use Heavy work for one page. Shared state or shared connection across tabs.
Connection Event Not needed. connect event.

Shared Worker vs Broadcast Channel

Feature Shared Worker Broadcast Channel
Shared Background Logic Yes. No, only messaging.
Can Keep Shared State Yes. Each tab manages its own state.
Can Maintain One WebSocket Yes. No.
Simplicity More complex. Simpler for tab messaging.
Best Use Shared compute, cache, or connection. Simple same-origin tab synchronization.

Shared Worker Limitations

  • Shared Workers cannot directly access the DOM.
  • They work only across same-origin browsing contexts.
  • Browser support may vary, especially on some mobile browsers.
  • They require message passing for all communication.
  • Worker lifetime is controlled by the browser.
  • They are not a replacement for persistent storage.
  • They should not store critical data only in memory.
  • Debugging can be less obvious than normal page scripts.

Feature Detection

if ("SharedWorker" in window) {
  console.log("Shared Workers are supported.");
} else {
  console.log("Use Broadcast Channel or local fallback.");
}

Use feature detection and provide fallbacks such as Broadcast Channel, Local Storage events, Service Workers, or duplicated per-tab workers.

Common Shared Worker Use Cases

  • Sharing one WebSocket connection across tabs.
  • Synchronizing same-origin application state.
  • Managing shared in-memory cache.
  • Coordinating notifications between tabs.
  • Running background calculations for multiple pages.
  • Reducing duplicate API requests across tabs.
  • Cross-tab activity tracking.
  • Micro frontend coordination on the same origin.

Shared Worker Best Practices

  • Use Shared Workers only when multiple contexts need shared logic.
  • Use structured message contracts with type and payload.
  • Validate all incoming messages before acting on them.
  • Keep worker logic focused and lightweight.
  • Provide fallbacks for unsupported browsers.
  • Do not rely on memory-only state for critical data.
  • Store important data in IndexedDB or on the server.
  • Handle errors and message serialization failures.

Security and Privacy Considerations

  • Shared Workers are same-origin, but messages should still be validated.
  • Do not store secrets only in worker memory.
  • Avoid sending sensitive data unless required.
  • Use HTTPS for production applications.
  • Validate message type, payload shape, and permissions.
  • Clean user-specific state on logout.
  • Be careful with shared WebSocket messages across multiple tabs.
  • Do not expose internal debug information through worker messages.

Common Shared Worker Mistakes

  • Using a Shared Worker when a Dedicated Worker is enough.
  • Forgetting to call port.start().
  • Assuming Shared Workers can access the DOM.
  • Not storing connected ports for broadcast scenarios.
  • Keeping critical data only in memory.
  • Not validating incoming message payloads.
  • Ignoring browser compatibility issues.
  • Creating complex shared state that is difficult to debug.

Key Takeaways

  • Shared Workers allow multiple same-origin contexts to share one worker.
  • Use new SharedWorker() to connect to a shared worker script.
  • Communication happens through worker.port and MessagePort.
  • The worker receives a connect event for every connected page.
  • Shared Workers are useful for shared state, shared cache, and one WebSocket connection.
  • Use feature detection and fallback strategies because browser support can vary.

Pro Tip

Use a Shared Worker when multiple tabs need one shared background process. For simple tab-to-tab messages, Broadcast Channel is often easier. For heavy work used by only one page, a Dedicated Worker is usually simpler.