javascriptgoogle-chromegoogle-chrome-extensionrequestxmlhttprequest

Persistent Service Worker in Chrome Extension


I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.

How can I keep it loaded and waiting until the request is intercepted?


Solution

  • Table of contents


    Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all its activities/requests after a certain time, which in Chrome is 5 minutes. The inactivity timer (i.e. when no such activities are ongoing) is even shorter: 30 seconds.

    Chromium team currently considers this behavior good (the team relaxes some aspects though occasionally e.g. Chrome 114 prolongs chrome.runtime port after each message), however this is only good for extensions that observe infrequent events, which run just a few times a day thus reducing browser's memory footprint between the runs (for example, webRequest/webNavigation events with urls filter for a rarely visited site). These extensions can be reworked to maintain the state, example. Unfortunately, such an idyll is unsustainable in many cases:


    ⭕ Bug exploit in Chrome 110+

    Calling any asynchronous chrome API keeps the worker running for 30 seconds more. The bug was officially considered a feature, so it should stay that way for several years before the team can admit their mistake and fix it.

    // background.js, keep running while doing something

    const keepAlive = (i => state => {
      if (state && !i) {
        if (performance.now() > 20e3) chrome.runtime.getPlatformInfo();
        i = setInterval(chrome.runtime.getPlatformInfo, 20e3);
      } else if (!state && i) {
        clearInterval(i);
        i = 0;
      }
    })();
    
    async function doSomething() {
      try {
        keepAlive(true);
        const res = await (await fetch('........')).text();
        // ...........
      } catch (err) {
        // ..........
      } finally {
        keepAlive(false);
      }
    }
    

    // background.js, keep running forever

    const keepAlive = () => setInterval(chrome.runtime.getPlatformInfo, 20e3);
    chrome.runtime.onStartup.addListener(keepAlive);
    keepAlive();
    

    ⭕ Offscreen API in Chrome 109+

    Courtesy of Keven Augusto.

    In Chrome 109 and newer you can use offscreen API to create an offscreen document and send some message from it every 30 second or less, to keep service worker running. Currently this document's lifetime is not limited (only audio playback is limited, which we don't use), but it's likely to change in the future.

    nativeMessaging in Chrome 105+

    In Chrome 105 and newer the service worker will run as long as it's connected to a nativeMessaging host via chrome.runtime.connectNative. If the host process is terminated due to a crash or user action, the port will be closed, and the SW will terminate as usual. You can guard against it by listening to port's onDisconnect event and call chrome.runtime.connectNative again.

    ⭕ WebSocket API in Chrome 116+

    Chrome 116 and newer: exchange WebSocket messages less than every 30 seconds to keep it active, e.g. every 25 seconds.

    ⭕ Pinging another tab

    Downsides:

    Warning! If you already connect ports, don't use this workaround, use another one for ports below.

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

    If you also use sendMessage

    In Chrome 99-101 you need to 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.

    ⭕ A dedicated tab

    Instead of using the SW, open a new tab with an extension page inside, so this page will act as a "visible background page" i.e. the only thing the SW would do is open this tab. You can also open it from the action popup.

    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:

    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

    Make sure you only enable the keep-alive for the duration of a critical task and disable it afterwards so that your extension doesn't unnecessarily consume the memory when unused.

    Save/restore the state (variables) in some storage to guard against a crash, example.

    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.