Persistent Service Worker in Chrome Extension Persistent Service Worker in Chrome Extension google-chrome google-chrome

Persistent Service Worker in Chrome Extension


In your case it's probably a bug in ManifestV3, https://crbug.com/1024211: the worker doesn't wake up for webRequest events. So just use ManifestV2 until this is fixed because there is no such thing as a persistent service worker.

Workarounds to prevent the extension's service worker from unloading:

0. Lite version: up to five minutes via waitUntil

  • Example 1, a hardcoded timeout:

    self.onactivate = e => {  e.waitUntil(new Promise(resolve => setTimeout(resolve, 5 * 60e3)));};
  • Example 2, waiting for a promise:

    let allDone;self.onactivate = e => e.waitUntil(new Promise(r => { allDone = r; })));//................chrome.some.event.addListener(() => {  foo().bar().finally(allDone);});

1. Keep alive forever via runtime ports

Open a runtime port from any tab's content script or from another page of the extension like the popup page. This port will live for five minutes (the inherent limit of a service worker) so you'll have to use a timer and the port's onDisconnect event to reconnect with some random tab again.

Downsides:

  • The need for a web page tab or an extension tab/popup.
  • Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.

Implementation example:

  • manifest.json, the relevant part:

      "permissions": ["scripting"],  "host_permissions": ["<all_urls>"],  "background": {"service_worker": "bg.js"}
  • background service worker bg.js:

    let lifeline;keepAlive();chrome.runtime.onConnect.addListener(port => {  if (port.name === 'keepAlive') {    lifeline = port;    setTimeout(keepAliveForced, 295e3); // 5 minutes minus 5 seconds    port.onDisconnect.addListener(keepAliveForced);  }});function keepAliveForced() {  lifeline?.disconnect();  lifeline = null;  keepAlive();}async function keepAlive() {  if (lifeline) return;  for (const tab of await chrome.tabs.query({ url: '*://*/*' })) {    try {      await chrome.scripting.executeScript({        target: { tabId: tab.id },        function: () => chrome.runtime.connect({ name: 'keepAlive' }),        // `function` will become `func` in Chrome 93+      });      chrome.tabs.onUpdated.removeListener(retryOnTabUpdate);      return;    } catch (e) {}  }  chrome.tabs.onUpdated.addListener(retryOnTabUpdate);}async function retryOnTabUpdate(tabId, info, tab) {  if (info.url && /^(file|https?):/.test(info.url)) {    keepAlive();  }}

2. Alternative approach: a dedicated tab

Open a new tab with an extension page inside e.g. chrome.tabs.create({url: 'bg.html'}).

It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).

Downsides:

  • consumes more memory,
  • wastes space in the tab strip,
  • distracts the user,
  • when multiple extensions open such a tab, the downsides snowball and become a real PITA.

You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.

Future of ManifestV3

Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.


If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:

  1. manifest v3
"permissions": [    "alarms"],
  1. service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })chrome.alarms.onAlarm.addListener(() => {  console.log('log for debug')});

Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:

// override.html<!DOCTYPE html><html lang="en">  <head>...<head>  <body>    ...    <script defer src="override.js"></script>  <body><html>
// override.js - this code is running in new tab pagenavigator.serviceWorker.getRegistrations().then((res) => {  for (let worker of res) {    console.log(worker)    if (worker.active.scriptURL.includes('background.js')) {      return    }  }  navigator.serviceWorker    .register(chrome.runtime.getURL('background.js'))    .then((registration) => {      console.log('Service worker success:', registration)    }).catch((error) => {      console.log('Error service:', error)    })})

This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.


WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.

I approached this problem by making sure that my service worker never ends, by adding the following code to it:

function keepServiceRunning() {    setTimeout(keepServiceRunning, 2000);  }keepServiceRunning()

After this, my callbacks now get invoked as expected.