Migrate to a service worker

Replacing background or event pages with a service worker

A service worker replaces the extension's background or event page to ensure that background code stays off the main thread. This enables extensions to run only when needed, saving resources.

Background pages have been a fundamental component of extensions since their introduction. To put it simply, background pages provide an environment that lives independent of any other window or tab. This allows extensions to observe and act in response to events.

This page describes tasks for converting background pages to extension service workers. For more information on extension service workers generally, see the tutorial Handle events with service workers and the section About extension service workers.

Differences between background scripts and extension service workers

In some contexts you'll see extension service workers called 'background scripts'. Although extension service workers do run in the background, calling them background scripts is somewhat misleading by implying identical capabilities. The differences are described below.

Changes from background pages

Service workers has a number of differences with background pages.

  • They function off the main thread, meaning they don't interfere with extension content.
  • They have special capabilities such as intercepting fetch events on the extension's origin, such as those from a toolbar popup.
  • They can communicate and interact with other contexts via the Clients interface.

Changes you'll need to make

You'll need to make a few code adjustments to account for differences between the way background scripts and service workers function. To start with, the way a service worker is specified in the manifest file is different from how background scripts are specified. Additionally:

  • Because they can't access the DOM or the window interface, you'll need to move such calls to a different API or into an offscreen document.
  • Event listeners should not be registered in response to returned promises or inside event callbacks.
  • Since they're not backward compatible with XMLHttpRequest() you'll need to replace calls to this interface with calls to fetch().
  • Since they terminate when not in use, you'll need to persist application states rather than rely on global variables. Terminating service workers can also end timers before they have completed. You'll need to replace them with alarms.

This page describes these tasks in detail.

Update the "background" field in the manifest

In Manifest V3, background pages are replaced by a service worker. The manifest changes are listed below.

  • Replace "background.scripts" with "background.service_worker" in the manifest.json. Note that the "service_worker" field takes a string, not an array of strings.
  • Remove "background.persistent" from the manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

The "service_worker" field takes a single string. You will only need the "type" field if you use ES modules (using the import keyword). Its value will always be "module". For more information, see Extension service worker basics

Move DOM and window calls to an offscreen document

Some extensions need access to the DOM and window objects without visually opening a new window or tab. The Offscreen API supports these use cases by opening and closing undisplayed documents packaged with extension, without disrupting the user experience. Except for message passing, offscreen documents do not share APIs with other extension contexts, but function as full web pages for extensions to interact with.

To use the Offscreen API, create an offscreen document from the service worker.

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

In the offscreen document perform any action you would previously have run in a background script. For example, you could copy text selected on the host page.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Communicate between offscreen documents and extension service workers using message passing.

Convert localStorage to another type

The web platform's Storage interface (accessible from window.localStorage) cannot be used in a service worker. To address this do one of two things. First, you can replace it with calls to another storage mechanism. The chrome.storage.local namespace will serve most use cases, but other options are available.

You can also move its calls to an offscreen document. For example, to migrate data previously stored in localStorage to another mechanism:

  1. Create an offscreen document with a conversion routine and a runtime.onMessage handler.
  2. Add a conversion routine to the offscreen document.
  3. In the extension service worker check chrome.storage for your data.
  4. If your data isn't found, create an offscreen document and call runtime.sendMessage() to start the conversion routine.
  5. In the runtime.onMessage handler that you added to the offscreen document, call the conversion routine.

There are also some nuances with how web storage APIs work in extensions. Learn more in Storage and Cookies.

Register listeners synchronously

Registering a listener asynchronously (for example inside a promise or callback) is not guaranteed to work in Manifest V3. Consider the following code.

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

This works with a persistent background page because the page is constantly running and never reinitialized. In Manifest V3, the service worker will be reinitialized when the event is dispatched. This means that when the event fires, the listeners will not be registered (since they are added asynchronously), and the event will be missed.

Instead, 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 startup logic.

chrome.action.onClicked.addListener(handleActionClick);

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

Replace XMLHttpRequest() with global fetch()

XMLHttpRequest() can't be called from a service worker, extension or otherwise. Replace calls from your background script to XMLHttpRequest() with calls to global fetch().

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Persist states

Service workers are ephemeral, which means they'll likely start, run, and terminate repeatedly during a user's browser session. It also means that data is not immediately available in global variables since the previous context was torn down. To get around this, use storage APIs as the source of truth. An example will show how to do this.

The following example uses a global variable to store a name. In a service worker, this variable could be reset multiple times over the course of a user's browser session.

Manifest V2 background script
let savedName = undefined;

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

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

For Manifest V3, replace the global variable with a call to Storage API.

Manifest V3 service worker
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

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

Convert timers to alarms

It's common to use delayed or periodic operations using the setTimeout() or setInterval() methods. These APIs can fail in service workers, though, because the timers are canceled whenever the service worker is terminated.

Manifest V2 background script
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Instead, use the Alarms API. As with other listeners, alarm listeners should be registered in the top level of your script.

Manifest V3 service worker
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

Keep the service worker alive

Service workers are by definition event-driven and will terminate on inactivity. This way Chrome can optimize performance and memory consumption of your extension. Learn more in our service worker lifecycle documentation. Exceptional cases might require additional measures to ensure that a service worker stays alive for a longer time.

Keep a service worker alive until a long-running operation is finished

During long running service worker operations that don't call extension APIs, the service worker might shut down mid operation. Examples include:

  • A fetch() request potentially taking longer than longer than five minutes (e.g. a large download on a potentially poor connection).
  • A complex asynchronous calculation taking more than 30 seconds.

To extend the service worker lifetime in these cases, you can periodically call a trivial extension API to reset the timeout counter. Please note, that this is only reserved for exceptional cases and in most situations there is usually a better, platform idiomatic, way to achieve the same result.

The following example shows a waitUntil() helper function that keeps your service worker alive until a given promise resolves:

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Keep a service worker alive continuously

In rare cases, it is necessary to extend the lifetime indefinitely. We have identified enterprise and education as the biggest use cases, and we specifically allow this there, but we do not support this in general. In these exceptional circumstances, keeping a service worker alive can be achieved by periodically calling a trivial extension API. It is important to note that this recommendation only applies to extensions running on managed devices for enterprise or education use cases. It is not allowed in other cases and the Chrome extension team reserves the right to take action against those extensions in the future.

Use the following code snippet to keep your service worker alive:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}