javascriptgoogle-chrome-extensionevent-loopchrome-extension-manifest-v3chrome-extension-manifest-v2

Why must MV3 Chrome extensions (using Service Workers) "register listeners in the first turn of the event loop"?


So I am in the process of migrating a MV2 extension which used persistent Background pages to MV3. In the Chrome migration guide [https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/#event_listeners] it says :

In order for Chrome to successfully dispatch events to the appropriate listeners, extensions must register listeners in the first turn of the event loop. The most straightforward way to achieve this is to move event registration to the top-level of your service worker script.

When a service worker is terminated, so are the event listeners associated with it. And since events are dispatched when a service worker starts, asynchronously registering events results in them being dropped because there's no listener registered when it is first spun up.

My question:

  1. Why do we have to register it like that? What's the issue if we register after awaiting an asynchronous operation?
  2. If indeed When a service worker is terminated, so are the event listeners associated with it, then how come an inactive service workers suddenly becomes active, if event listeners are all terminated? (I assume it isn't listening for events if event listeners are terminated.)
// background.js(service worker)
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });

  // Listener is registered asynchronously
  // This is NOT guaranteed to work in Manifest V3/service workers! Dont do this
  chrome.action.onClicked.addListener(handleActionClick);
});

Solution

  • Why do we have to register it like that ?

    Because this is how event registration is implemented internally to let the browser know which types of events should wake up the worker:

    1. When the background service worker script wasn't running, it's started in a new JS environment and the entire script runs from the first line to the last.
    2. When it calls addListener for chrome events, the API stores the function reference internally and tells the browser to remember this event name in the internal browser database.
    3. After the last statement has finished executing, the browser calls the JS functions for the events remembered by the browser in the previous run of the worker (step 2 above).
    4. After all open ports are closed and all events are processed the inactivity timer fires (30 seconds in Chrome) and the script is terminated, the JS environment is entirely destroyed.

    Whats the issue if we register after awaiting an asynchronous operation ?

    When the background service worker script wasn't running before the event, it's started as described above and your belatedly registered listener won't be present in the internal API database, so it won't be called and this event will be lost for you.

    Only when the background service worker script is already running before the event is fired, your belatedly registered listener will see the event.

    If indeed When a service worker is terminated, so are the event listeners associated with it then how come an inactive service workers become active suddenly ,if event listeners are all terminated ?(I assume it isnt listening for events if event listeners are terminated .)

    Indeed, when the background service worker script is terminated it's like closing of a tab, nothing survives inside, it doesn't listen to anything, there's no "it" anymore.

    The wake-up is implemented by addListener telling the browser to remember the event names and their filters (e.g. in webRequest of webNavigation) in the internal database of the browser process, which happens every time the background service worker script runs (but only in the first turn of the event loop). When an event matching the criteria occurs in the browser, the browser starts the extension's background service worker script and dispatches the event as described in the beginning of this answer.