Migrate to a service worker: Manifest v2 to v3

Update manifest v2 to v3: Migrate to a service worker

Replacing background or event pages with a service worker (Manifest v2 to v3). Created by Exmo – a monetization platform for browser extensions.


Previous post


Note: Manifest V3 is generally supported in Chrome 88 or later. For extension features added in later Chrome versions, refer to the API reference documentation for support information. If your extension relies on a specific API, you can specify a minimum chrome version in the manifest file.

A service worker replaces the extension’s background or event page to ensure that background code operates off the main thread. This enables extensions to run only when necessary, thus conserving resources.

Background pages have been a fundamental component of extensions since their inception. Essentially, background pages provide an environment independent of any other window or tab, allowing extensions to observe and respond to events.

This guide outlines the tasks for converting background pages to extension service workers. For more information on extension service workers, refer to 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, extension service workers may be referred to as ‘background scripts.’ While extension service workers do operate in the background, labeling them as background scripts can be somewhat misleading as it implies identical capabilities. The differences are outlined below.

Changes from background pages

Service workers have several differences compared to background pages:

  • They operate off the main thread, preventing interference with extension content.
  • They possess 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 several code adjustments to accommodate the differences between background scripts and service workers. Initially, the way a service worker is specified in the manifest file differs from how background scripts are specified. Additionally:

  • Calls to the DOM or the window interface cannot be made since service workers lack access to them. Such calls need to be moved to a different API or into an offscreen document.
  • Event listeners should not be registered in response to returned promises or inside event callbacks.
  • Calls to the XMLHttpRequest() interface should be replaced with calls to fetch() since service workers are not backward compatible with XMLHttpRequest().
  • Since service workers terminate when not in use, application states need to be persisted rather than relying on global variables. Terminating service workers can also prematurely end timers, requiring them to be replaced with alarms.

Updating the “background” field in the manifest

In Manifest V3, background pages are replaced by a service worker. The manifest changes are as follows:

  • 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 accepts a single string. You will only need the “type” field if you use ES modules (using the import keyword). Its value should always be “module”. For more information, refer to Extension service worker basics.


Manifest v2 to v3: Move DOM and window calls to an offscreen document

Some extensions require access to the DOM and window objects without visibly opening a new window or tab. The Offscreen API supports these scenarios by allowing extensions to open and close hidden documents, packaged with the 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, carry out any actions that you would have previously performed in a background script. For instance, you could copy the text selected on the host page.

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

Use message passing to facilitate communication between offscreen documents and extension service workers.

Convert localStorage to Another Storage Type

The web platform’s Storage interface (accessed via window.localStorage) cannot be used in a service worker. To address this, you have two options:

  1. Replace localStorage with Another Storage Mechanism:
    • The chrome.storage.local namespace can handle most use cases, though other options are also available.
  2. Move localStorage Calls to an Offscreen Document:
    • To migrate data previously stored in localStorage to another mechanism, follow these steps:

Steps to Migrate Data:

  1. Create an Offscreen Document:
    • Create an offscreen document with a conversion routine and a runtime.onMessage handler.
  2. Add a Conversion Routine:
  3. Check for Data in chrome.storage:
    • In the extension service worker, check chrome.storage for your data.
  4. Create Offscreen Document if Data is Not Found:
    • If the data isn’t found, create an offscreen document and use runtime.sendMessage() to initiate the conversion routine.
  5. Handle runtime.onMessage:
    • In the runtime.onMessage handler in the offscreen document, call the conversion routine to migrate the data.

Additionally, there are some nuances regarding how web storage APIs work in extensions. For more information, refer to the documentation on Storage and Cookies.

By following these steps, you can effectively migrate and manage your extension’s storage without relying on localStorage within a service worker.

Register Listeners Synchronously

Registering a listener asynchronously (e.g., 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 code works with a persistent background page because the page is constantly running and never reinitialized. However, in Manifest V3, the service worker is reinitialized when the event is dispatched. This means that when the event fires, the listeners may not be registered (since they are added asynchronously), causing the event to be missed.

To avoid this issue, move the event listener registration to the top level of your script. This ensures that Chrome can immediately find and invoke your action’s click handler, even if your extension hasn’t finished executing its startup logic.

Here is the revised code:

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

By registering listeners synchronously at the top level of your script, you ensure that your extension’s event handlers are ready to be invoked as soon as the events are dispatched.


Replace XMLHttpRequest() with fetch()

XMLHttpRequest() cannot be called from a service worker, whether it’s part of an extension or not. To adapt, replace any calls to XMLHttpRequest() in your background script with calls to the global fetch() function.

Here’s how to convert your code:

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);
Key Point: If you previously utilized XMLHttpRequest() or fetch() to fetch executable code, this approach is no longer viable. All executable code must now be integrated within your extension package for enhanced security measures. For additional details, refer to the guidelines provided in the "Improve extension security" documentation.

Persisting States

Service workers are ephemeral, meaning they can start, run, and terminate repeatedly during a user’s browser session. Consequently, data is not readily available in global variables since the previous context was torn down. To address this, leverage storage APIs as the source of truth. The following example demonstrates how to achieve this:

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 the 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 });
});

By utilizing the Storage API in Manifest V3, you can ensure that states are persisted across service worker instances, thereby maintaining consistency and reliability in your extension.


Converting Timers to Alarms

It’s common to utilize delayed or periodic operations using the setTimeout() or setInterval() methods. However, these APIs can fail in service workers because the timers are canceled whenever the service worker is terminated. To address this, consider using the Alarms API.

Manifest V2 Background Script

// 3 minutes in milliseconds

const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

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(),
  });
});

Ensure that alarm listeners are registered at the top level of your script, similar to other listeners. By employing the Alarms API in Manifest V3, you can ensure the execution of delayed or periodic operations without interruptions due to service worker termination.


Ensuring Service Worker Longevity

Service workers are inherently event-driven and may terminate due to inactivity to optimize the performance and memory consumption of your extension. However, exceptional cases may necessitate additional measures to prolong the lifespan of a service worker.

Keep a Service Worker Alive During Long-Running Operations

During lengthy service worker operations that do not invoke extension APIs, there is a risk of the service worker shutting down prematurely. Examples include:

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

To extend the service worker’s lifetime in such cases, you can periodically call a trivial extension API to reset the timeout counter. Note that this approach is reserved for exceptional cases, and in most scenarios, there are better, platform-idiomatic methods to achieve the same result.

The following example demonstrates a waitUntil() helper function that ensures your service worker remains active 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());
Key Point: An official API similar to waitUntil() is currently under discussion in the WECG. For further details, refer to the ongoing discussion on GitHub.

Maintaining Continuous Service Worker Operation

In exceptional circumstances, such as enterprise or educational use cases, there may be a need to indefinitely extend the lifetime of a service worker. While this practice is specifically permitted for such scenarios, it is not supported for general use. It’s essential to note that the Chrome extension team reserves the right to take action against extensions that employ this technique outside of approved contexts.

To keep a service worker alive continuously, you can periodically call a trivial extension API. Here’s a code snippet demonstrating how to implement this approach:

/**

 * Tracks the last time a service worker was active and extends its lifetime by
 * updating extension storage with the current time every 20 seconds.
 * It's still important to handle unexpected termination, such as if the
 * extension process crashes or the service worker is manually stopped.
 */

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

/**
 * Initiates the heartbeat interval to maintain service worker activity.
 * Use this method sparingly when performing tasks that require persistence,
 * and call stopHeartbeat once those tasks are completed.
 */

async function startHeartbeat() {

  // Perform the heartbeat once at service worker startup.
  runHeartbeat().then(() => {

    // Then repeat every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}
async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Retrieves the timestamp of the last heartbeat stored in extension storages,
 * or returns undefined if the heartbeat has not been recorded before.
 */

async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}

By utilizing this mechanism, you can ensure the continuous operation of your service worker in specific sanctioned environments, such as enterprise or educational settings.


Continue reading:

monetize browser extension

Frequently Asked Questions (FAQ)

What is a manifest in the context of Chrome extensions?

A manifest in the context of Chrome extensions is a JSON file named manifest.json that includes essential information about the extension such as its name, version, permissions, and other details required for it to run properly.

How to migrate a Chrome extension from manifest v2 to manifest v3?

To migrate a Chrome extension from manifest v2 to manifest v3, developers need to update their manifest file according to the specifications of manifest version 3. This may involve changing the structure of the manifest and adjusting any APIs used in the extension.

What are permissions in a Chrome extension manifest?

Permissions in a Chrome extension manifest specify which APIs and resources the extension can access. Developers need to declare the necessary permissions in the manifest to ensure that the extension functions correctly without any security issues.

What is a service worker in the context of Chrome extensions?

A service worker is a script that runs in the background of a browser and helps the extension manage network requests, caching, and other tasks. Using a service worker can improve the performance and responsiveness of the extension.

How to implement browser actions in a Chrome extension?

Browser actions in a Chrome extension are UI elements like buttons that appear in the browser’s toolbar. Developers can define browser_action properties in the manifest to create and customize the appearance and behavior of these elements.

What is content security policy (CSP) in the context of Chrome extensions?

Content Security Policy (CSP) is a set of directives that help prevent common vulnerabilities in web applications, including extensions. By configuring CSP headers in the manifest or headers, developers can mitigate risks such as XSS attacks and data injection.

What are some common challenges in upgrading Chrome extensions to manifest v3?

Upgrading Chrome extensions to manifest v3 may pose challenges such as adapting to the new.

ruslana-gonsalez

As an Exmo Product Manager, my role involves overseeing the development and enhancement of our monetization platform. I lead a team of specialists, strategizing innovative features and improvements to optimize user experience. My responsibilities include conducting market research, gathering user feedback, and collaborating with developers to ensure Exmo remains at the forefront of browser extension monetization.