Persistent Service Worker in Chrome Extension

We Are Going To Discuss About Persistent Service Worker in Chrome Extension. So lets Start this Javascript Article.

Persistent Service Worker in Chrome Extension

How to solve Persistent Service Worker in Chrome Extension

This is caused by these problems in ManifestV3:
crbug.com/1024211, the worker doesn’t wake up for webRequest events.
The workarounds are listed below.
crbug.com/1271154, the worker is randomly broken after an update.
Mostly fixed in Chrome 101.
Service worker (SW) can’t be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or ports after some time, which in Chrome is 5 minutes. Chromium team currently considers this behavior intentional and good for extensions.
This behavior is bad in case an extension has to observe frequent events:
chrome.tabs.onUpdated/onActivated,
chrome.webNavigation if not scoped to a rare url,
chrome.webRequest if not scoped to a rare url or type,
chrome.runtime.onMessage/onConnect for messages from content script in all tabs.
Such events are generated in response to user actions, so there are natural pauses for a few minutes, during which the SW is terminated, then it starts again for a new event, which takes 50ms (creating the process) plus 5…1000ms (running the entire SW script), so it’s 50…1000 times heavier than calling a simple JS event listener (1ms) in an idle background script.
So, SW may restart hundreds of times a day thus wearing down CPU/disk/battery and often introducing a frequent perceivable lag of the extension’s reaction.
Workaround for webRequest not waking up
Additionally subscribe to an API like chrome.webNavigation as shown in the other answer(s).
This applies to extensions that observe infrequent events, e.g. you specified urls filter for webRequest/webNavigation for just one rarely visited site. Such extensions can be reworked to avoid the need for a persistent background script so it will start only a few times a day, which will be good for memory footprint while not stressing CPU too much. You would save/load the variables/state in each listener via chrome.storage.session (temporary, 1MB max), or chrome.storage.local, or even IndexedDB that’s much faster for big/complex data.
But if you MUST observe frequent events (listed in the beginning of this answer), you’ll have to prolong the background script’s lifetime artificially using the following workarounds.
“Persistent” service worker while a connectable tab is present
In case you don’t use ports with a content script that runs in all tabs (shown in another workaround below), here’s an example of opening a runtime port from any tab’s content script or from another page of the extension like the popup page and reconnecting it before 5 minutes elapse.
Downsides:
The need for an open web page tab or an open 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.
Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.
Warning! You don’t need this if you already use the workaround for chrome.runtime.connect (below) with a content script that runs in all tabs.
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(); } }
If you also use sendMessage
Always call sendResponse() in your chrome.runtime.onMessage listener even if you don’t need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.
If you already use ports e.g. chrome.runtime.connect
Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you’ll still need to reconnect them, because their 5-minute lifetime hasn’t changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.
background script example:
chrome.runtime.onConnect.addListener(port => { if (port.name !== 'foo') return; port.onMessage.addListener(onMessage); port.onDisconnect.addListener(deleteTimer); port._timer = setTimeout(forceReconnect, 250e3, port); }); function onMessage(msg, port) { console.log('received', msg, 'from', port.sender); } function forceReconnect(port) { deleteTimer(port); port.disconnect(); } function deleteTimer(port) { if (port._timer) { clearTimeout(port._timer); delete port._timer; } }
client script example e.g. a content script:
let port; function connect() { port = chrome.runtime.connect({name: 'foo'}); port.onDisconnect.addListener(connect); port.onMessage.addListener(msg => { console.log('received', msg, 'from bg'); }); } connect();
“Forever”, via a dedicated tab, while the tab is open
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.
Caution regarding persistence
You still need to save/restore the state (variables) because there’s no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage like chrome.storage.session if 1MB is sufficient or chrome.storage.local or IndexedDB for types like Blob, ArrayBuffer, UInt8Array.
Note that you shouldn’t make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.
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.

Persistent Service Worker in Chrome Extension

This is caused by these problems in ManifestV3:
crbug.com/1024211, the worker doesn’t wake up for webRequest events.
The workarounds are listed below.
crbug.com/1271154, the worker is randomly broken after an update.
Mostly fixed in Chrome 101.
Service worker (SW) can’t be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or ports after some time, which in Chrome is 5 minutes. Chromium team currently considers this behavior intentional and good for extensions.
This behavior is bad in case an extension has to observe frequent events:
chrome.tabs.onUpdated/onActivated,
chrome.webNavigation if not scoped to a rare url,
chrome.webRequest if not scoped to a rare url or type,
chrome.runtime.onMessage/onConnect for messages from content script in all tabs.
Such events are generated in response to user actions, so there are natural pauses for a few minutes, during which the SW is terminated, then it starts again for a new event, which takes 50ms (creating the process) plus 5…1000ms (running the entire SW script), so it’s 50…1000 times heavier than calling a simple JS event listener (1ms) in an idle background script.
So, SW may restart hundreds of times a day thus wearing down CPU/disk/battery and often introducing a frequent perceivable lag of the extension’s reaction.
Workaround for webRequest not waking up
Additionally subscribe to an API like chrome.webNavigation as shown in the other answer(s).
This applies to extensions that observe infrequent events, e.g. you specified urls filter for webRequest/webNavigation for just one rarely visited site. Such extensions can be reworked to avoid the need for a persistent background script so it will start only a few times a day, which will be good for memory footprint while not stressing CPU too much. You would save/load the variables/state in each listener via chrome.storage.session (temporary, 1MB max), or chrome.storage.local, or even IndexedDB that’s much faster for big/complex data.
But if you MUST observe frequent events (listed in the beginning of this answer), you’ll have to prolong the background script’s lifetime artificially using the following workarounds.
“Persistent” service worker while a connectable tab is present
In case you don’t use ports with a content script that runs in all tabs (shown in another workaround below), here’s an example of opening a runtime port from any tab’s content script or from another page of the extension like the popup page and reconnecting it before 5 minutes elapse.
Downsides:
The need for an open web page tab or an open 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.
Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.
Warning! You don’t need this if you already use the workaround for chrome.runtime.connect (below) with a content script that runs in all tabs.
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(); } }
If you also use sendMessage
Always call sendResponse() in your chrome.runtime.onMessage listener even if you don’t need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.
If you already use ports e.g. chrome.runtime.connect
Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you’ll still need to reconnect them, because their 5-minute lifetime hasn’t changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.
background script example:
chrome.runtime.onConnect.addListener(port => { if (port.name !== 'foo') return; port.onMessage.addListener(onMessage); port.onDisconnect.addListener(deleteTimer); port._timer = setTimeout(forceReconnect, 250e3, port); }); function onMessage(msg, port) { console.log('received', msg, 'from', port.sender); } function forceReconnect(port) { deleteTimer(port); port.disconnect(); } function deleteTimer(port) { if (port._timer) { clearTimeout(port._timer); delete port._timer; } }
client script example e.g. a content script:
let port; function connect() { port = chrome.runtime.connect({name: 'foo'}); port.onDisconnect.addListener(connect); port.onMessage.addListener(msg => { console.log('received', msg, 'from bg'); }); } connect();
“Forever”, via a dedicated tab, while the tab is open
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.
Caution regarding persistence
You still need to save/restore the state (variables) because there’s no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage like chrome.storage.session if 1MB is sufficient or chrome.storage.local or IndexedDB for types like Blob, ArrayBuffer, UInt8Array.
Note that you shouldn’t make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.
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.

Solution 1

This is caused by these problems in ManifestV3:

  • crbug.com/1024211, the worker doesn’t wake up for webRequest events.
    The workarounds are listed below.

  • crbug.com/1271154, the worker is randomly broken after an update.
    Mostly fixed in Chrome 101.

  • Service worker (SW) can’t be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or ports after some time, which in Chrome is 5 minutes. Chromium team currently considers this behavior intentional and good for extensions.

    This behavior is bad in case an extension has to observe frequent events:

    • chrome.tabs.onUpdated/onActivated,
    • chrome.webNavigation if not scoped to a rare url,
    • chrome.webRequest if not scoped to a rare url or type,
    • chrome.runtime.onMessage/onConnect for messages from content script in all tabs.

    Such events are generated in response to user actions, so there are natural pauses for a few minutes, during which the SW is terminated, then it starts again for a new event, which takes 50ms (creating the process) plus 5…1000ms (running the entire SW script), so it’s 50…1000 times heavier than calling a simple JS event listener (1ms) in an idle background script.

    So, SW may restart hundreds of times a day thus wearing down CPU/disk/battery and often introducing a frequent perceivable lag of the extension’s reaction.

Workaround for webRequest not waking up

Additionally subscribe to an API like chrome.webNavigation as shown in the other answer(s).

This applies to extensions that observe infrequent events, e.g. you specified urls filter for webRequest/webNavigation for just one rarely visited site. Such extensions can be reworked to avoid the need for a persistent background script so it will start only a few times a day, which will be good for memory footprint while not stressing CPU too much. You would save/load the variables/state in each listener via chrome.storage.session (temporary, 1MB max), or chrome.storage.local, or even IndexedDB that’s much faster for big/complex data.

But if you MUST observe frequent events (listed in the beginning of this answer), you’ll have to prolong the background script’s lifetime artificially using the following workarounds.

“Persistent” service worker while a connectable tab is present

In case you don’t use ports with a content script that runs in all tabs (shown in another workaround below), here’s an example of opening a runtime port from any tab’s content script or from another page of the extension like the popup page and reconnecting it before 5 minutes elapse.

Downsides:

  • The need for an open web page tab or an open 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.

Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.

Warning! You don’t need this if you already use the workaround for chrome.runtime.connect (below) with a content script that runs in all tabs.

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

If you also use sendMessage

Always call sendResponse() in your chrome.runtime.onMessage listener even if you don’t need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.

If you already use ports e.g. chrome.runtime.connect

Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you’ll still need to reconnect them, because their 5-minute lifetime hasn’t changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.

  • background script example:

    chrome.runtime.onConnect.addListener(port => {
      if (port.name !== 'foo') return;
      port.onMessage.addListener(onMessage);
      port.onDisconnect.addListener(deleteTimer);
      port._timer = setTimeout(forceReconnect, 250e3, port);
    });
    function onMessage(msg, port) {
      console.log('received', msg, 'from', port.sender);
    }
    function forceReconnect(port) {
      deleteTimer(port);
      port.disconnect();
    }
    function deleteTimer(port) {
      if (port._timer) {
        clearTimeout(port._timer);
        delete port._timer;
      }
    }
    
  • client script example e.g. a content script:

    let port;
    function connect() {
      port = chrome.runtime.connect({name: 'foo'});
      port.onDisconnect.addListener(connect);
      port.onMessage.addListener(msg => {
        console.log('received', msg, 'from bg');
      });
    }
    connect();
    

“Forever”, via a dedicated tab, while the tab is open

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.

Caution regarding persistence

You still need to save/restore the state (variables) because there’s no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage like chrome.storage.session if 1MB is sufficient or chrome.storage.local or IndexedDB for types like Blob, ArrayBuffer, UInt8Array.

Note that you shouldn’t make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.

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.

Original Author wOxxOm Of This Content

Solution 2

unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.

chrome.webNavigation.onBeforeNavigate.addListener(function(){

   chrome.webRequest.onResponseStarted.addListener(function(details){

      //.............
      
      //.............

   },{urls: ["*://domain/*"],types: ["main_frame"]});


},{
    url: [{hostContains:"domain"}]
});

Original Author Clairzil Bawon samdi Of This Content

Solution 3

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 page
navigator.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.

Original Author Valery Kiseliou Of This Content

Solution 4

As Clairzil Bawon samdi’s answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:

// manifest.json
...
"background": {
  "service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...

In my case it listens onHistoryStateUpdated event to wake up the service worker:

// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
  console.log('wake me up');
});

chrome.webRequest.onSendHeaders.addListener(
  (details) => {
    // code here
  },
  {
    urls: ['https://example.com/api/*'],
    types: ['xmlhttprequest'],
  },
  ['requestHeaders']
);

Original Author Agung Darmanto Of This Content

Conclusion

So This is all About This Tutorial. Hope This Tutorial Helped You. Thank You.

Also Read,

ittutorial team

I am an Information Technology Engineer. I have Completed my MCA And I have 4 Year Plus Experience, I am a web developer with knowledge of multiple back-end platforms Like PHP, Node.js, Python and frontend JavaScript frameworks Like Angular, React, and Vue.

Leave a Comment