Migrating from Background Pages to Service Workers

Background pages have been a fundamental component of the Chrome Extension platform since its introduction. To put it simply, background pages provide extension authors with an environment that lives independent of any other window or tab. This allows extensions to observe and take action in response to events.

In manifest version 3, the Chrome extension platform will move from background pages to service workers. As stated in Service Workers: an Introduction, a "service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction." This is the technology that enables native-like experiences such as push notifications, rich offline support, background sync, and "Add to Home Screen" on the open web. Service workers were inspired in part by background pages in Chrome Extensions, but they iterate and improve on this model by tuning it for web-scale.

When migrating to this new background context, you'll need to keep two main things in mind. First, service workers are terminated when not in use and restarted when needed (similar to event pages). Second, service workers don't have access to DOM. We'll explore how to adapt to these challenges in the Thinking with Events and Working with Workers sections below, respectively. If you already use an event page, skip straight to the second section.

Thinking With Events

Like event pages, service workers are a special execution environment that are started to handle events they're interested in and are terminated when they're no longer needed. The following sections provide recommendations for writing code in an ephemeral, evented execution context.

Note: Several of these concepts are covered in the Migrate to Event Driven Background Scripts article.

Top Level Event Listeners

In order for Chrome to successfully dispatch events to the appropriate listeners, extensions must register listeners in the first turn of the event loop. The most straightforward way to achieve this is to move event registration to the top-level of your service worker script.

The below snippet shows how an existing extension initializes its browser action listener in a persistent background page.

chrome.storage.local.get(['badgeText'], ({badgeText}) => {
  chrome.browserAction.setBadgeText({text: badgeText});

  // Listener is registered asynchronously.
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

While this approach works in a persistent background page, it is not guaranteed to work in a service worker due to the asynchronous nature of chrome.storage APIs. When a service worker is terminated, so are the event listeners associated with it. And since events are dispatched when a service worker starts, asynchronously registering events results in them being dropped because there's no listener registered when it is first spun up.

To address this, move the event listener registration to the top level of your script. This ensures that Chrome will be able to immediately find and invoke your action's click handler, even if your extension hasn't finished executing its async startup logic.

chrome.storage.local.get(['badgeText'], ({badgeText}) => {
  chrome.action.setBadgeText({text: badgeText});
});

// Listener is registered on on startup.
chrome.action.onClicked.addListener(handleActionClick);

Note: In manifest version 3 the chrome.browserAction and chrome.pageAction APIs are consolidated into a single chrome.action API.

Persisting State With Storage APIs

One of the main things to get used to when adopting service workers is that they are short-lived execution environments. In more practical terms, an extension's service worker will start up, do some work, and get terminated repeatedly throughout a user's browser session. This poses a challenge to extension developers accustomed to long-lived background pages as application data is not immediately available in global variables.

This example is taken from a simple manifest version 2 extension that receives a name from a content script and persists it in a global variable for later use.

let name = undefined;

chrome.runtime.onMessage.addListener(({type, name}) => {
  if (msg.type === 'set-name') {
    name = msg.name;
  }
});

chrome.browserAction.onClicked.addListener(tab => {
  chrome.tabs.sendMessage(tab.id, {name});
});

If we port this code directly to a service worker, it's possible that the service worker will be terminated between when the name is set and the user clicks the browser action. If this happens, the set name will have been lost and name variable will again be undefined.

We can fix this bug by treating the storage APIs as our source of truth.

chrome.runtime.onMessage.addListener(({type, name}) => {
  if (type === 'set-name') {
    chrome.storage.local.set({name});
  }
});

chrome.action.onClicked.addListener(tab => {
  chrome.storage.local.get(['name'], ({name}) => {
    chrome.tabs.sendMessage(tab.id, {name});
  });
});

Note: In manifest version 3 the chrome.browserAction and chrome.pageAction APIs are consolidated into a single chrome.action API.

Moving From Timers to Alarms

It's common for web developers to perform delayed or periodic operations using the setTimeout or setInterval methods. These APIs fail in service workers though, because the scheduler will cancel the timers when the service worker is terminated.

const TIMEOUT = 3 * 60 * 1000;  // 3 minutes in milliseconds
window.setTimeout(function() {
  chrome.action.setIcon({
    path: getRandomIconPath()
  });
}, TIMEOUT);

Instead, we can use the alarms API. Like other listeners, alarm listeners should be registered in the top level of your script.

chrome.alarms.create({delayInMinutes: 3.0});

chrome.alarms.onAlarm.addListener(function() {
  chrome.action.setIcon({
    path: getRandomIconPath()
  });
});

Working with Workers

Service workers are a specialized kind of web worker, which are quite different from the web pages most web developers are used to working with. On a typical web page (or extension background page), the global execution context for JavaScript is a Window. This object exposes the capabilities that web developers are used to working with: window, element, IndexedDB, cookie, localStorage, fetch, etc. The global scope for service worker is significantly more limited and doesn't have many of these features. Most notably, service workers don't have access to the DOM.

The following sections cover some of the major use cases impacted by the move to service workers and recommendations on how to adapt.

Parsing and Traversing with XML/HTML

Since service workers don't have access to DOM, it's not possible for an extension's service worker to access the DOMParser API or create iframes to parse and traverse documents. Extension developers have two ways to work around this limitation: create a new tab or use a library. Which you choose will depend on your use case.

Libraries such as jsdom can be used to emulate a typical browser window environment, complete with DOMParser, event propagation, and other capabilities like requestAnimationFrame. Lighter weight alternatives like undom provide just enough DOM to power many frontend frameworks and libraries.

Extensions that need a full native browser environment can use the chrome.windows.create() and chrome.tabs.create() APIs from inside a service worker. Additionally, an extension's popup still provides a full (temporary) window environment.

Audio/Video Playback and Capture

It's not currently possible to play or capture media directly in a service worker. In order for a manifest version 3 extension to leverage the web's media playback and capture capabilities, the extension will need to create a window environment using either chrome.windows.create() or chrome.tabs.create(). Once created, the extension can use message passing to coordinate between the playback document and service worker.

Rendering to a Canvas

In some cases developers use background pages to render content for display in other contexts or to create and cache assets. While service workers don't have access to DOM and therefore cannot use <canvas> elements, service workers do have access to the OffscreenCanvas API.

function makeCanvas(width, height) {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  return canvas;
}

In the above block we're constructing a canvas element and painting the entire canvas turquoise. To migrate to offscreen canvas, replace document.createElement('canvas') with new OffscreenCanvas(width, height).

function makeCanvas(width, height) {
  const canvas = new OffscreenCanvas(width, height);
  return canvas;
}

For additional guidance on working with OffscreenCanvas, see OffscreenCanvas — Speed up Your Canvas Operations with a Web Worker.