MV3 extensions constantly reloading

90 views
Skip to first unread message

Addon Land

unread,
May 21, 2024, 2:37:00 PMMay 21
to chromium-...@chromium.org
I was reviewing the MV3 extensions I have installed and how they behave.

Of the 10 that go inactive when nothing significant happens for 30 seconds, half of them reload the service worker as soon as I navigate anywhere, even navigating within a page such that it changes the fragment identifier.

Presumably this is because these extensions have something like webrequest or chrome.tabs.onUpdated listeners.

So if I read a web page for 30 seconds (unload), then click on a link, all these service workers reload.  I browse social media for a short period (unload), then open a new tab, reload.  Respond to an email for 30 seconds (unload) and open a new email, reload.  Turn around to pet the dog, unload, click on anything in the browser, reload.

I'm struggling to see how this can be better than having those extensions stay loaded?

It seems like it would be a best practice to force your extension to stay loaded if it is handlineg events that will be called frequently during normal browsing activity.

woxxom

unread,
May 21, 2024, 6:09:28 PMMay 21
to Chromium Extensions, Addon Land
The scenario you've described is very popular and there are lots of extensions with a background script that reacts to navigation. There are also many extensions that take a lot of time to initialize the state necessary to make a decision which may take more than 1 second every time the background script restarts, for example extensions based on `wedata` pagination rules like Autopagerize. It was pointed out multiple times by multiple developers for many years.

Here's what the documentation says:

-----------------

> During long running service worker operations that don't call extension APIs, the service worker might shut down mid operation. Examples include:

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

> To extend the service worker lifetime in these cases, you can periodically call a trivial extension API to reset the timeout counter. Please note, that this is only reserved for exceptional cases and in most situations there is usually a better, platform idiomatic, way to achieve the same result.

> In rare cases, it is necessary to extend the lifetime indefinitely. We have identified enterprise and education as the biggest use cases, and we specifically allow this there, but we do not support this in general. In these exceptional circumstances, keeping a service worker alive can be achieved by periodically calling a trivial extension API. It is important to note that this recommendation only applies to extensions running on managed devices for enterprise or education use cases. It is not allowed in other cases and the Chrome extension team reserves the right to take action against those extensions in the future.

-----------------

While this is a step in the right direction compared to their initial stance when ManifestV3 was announced, this is still not based on a comprehensive investigation of how extensions actually work in real life. The last bit is a particularly deplorable combination of bureaucratic pompousness with vindictive pettiness. How about adding frequency of such restarts as well as the CPU load of the initialization phase in chrome://histograms instead?

Most extensions consume ~35MB of RAM in their background context which is just 10% of a modern site's tab like youtube, so it's not an urgent problem for most extensions that needs to be fixed, although of course there are exceptions that consume 10 times more, but in those cases I'd argue that when the state is so big unloading it after 30 seconds is too aggressive for something that is likely to take a lot of time to rebuild. It should be adaptive depending on the initialization duration, frequency of restarts, and the amount of free RAM.

The content scripts are actually a bigger problem as the recent study shows, because lots of extensions wastefully load a lot of code into every tab (plus there's no code compilation cache, so everything is re-interpreted in every tab and frame), even when it's only used on demand e.g. after selecting something or clicking. Such extensions should inject only a tiny script with the listener that loads the rest of the code on user interaction. The documentation should add examples of doing it via web_accessible_resources + dynamic import() and via chrome.runtime.sendMessage + chrome.scripting.executeScript.

That said, there are arguably many incorrectly written extensions that observe navigation of all URLs unnecessarily instead of registering targeted listeners. There's no magic solution, it's something that needs to be nurtured carefully by providing more info in the documentation, with examples of typical mistakes from real extensions and how to re-write them. An example of such mistake would be chrome.tabs.onUpdated + checking tab.url inside to inject in one or a few domains instead of using chrome.webNavigation or chrome.webRequest with a URL filter.

woxxom

unread,
May 22, 2024, 3:55:22 AMMay 22
to Chromium Extensions, woxxom, Addon Land
Hey, even the official examples are incorrectly written, so what can we expect from extension developers in general? The cookbook.sidepanel-site-specific example uses chrome.tabs.onUpdated + checking tab.url inside and wastefully runs on every navigation. Here's how it can be fixed:

const HOSTS = ['www.google.com'];

chrome.runtime.onStartup.addListener(checkExistingTabs);
chrome.runtime.onInstalled.addListener(info => {
  if (info.reason === 'update' || info.reason === 'install') {
    chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true});
    chrome.sidePanel.setOptions({enabled: false});
    checkExistingTabs();
  }
});
chrome.webNavigation.onCommitted.addListener(info => {
  if (!info.frameId) enableForTabId(info.tabId);
}, { url: HOSTS.map(h => ({hostEquals: h})) });

async function checkExistingTabs() {
  for (const tab of await chrome.tabs.query({ url: HOSTS.map(h => `*://${h}/*`) })) {
    enableForTabId(tab.id);
  }
}

function enableForTabId(tabId) {
  return chrome.sidePanel.setOptions({
    tabId,
    enabled: true,
    path: 'sidepanel.html',
  });
}

woxxom

unread,
May 22, 2024, 4:15:48 AMMay 22
to Chromium Extensions, woxxom, Addon Land
Alas, it's not that easy, because the extension needs to disable the side panel when the previously enabled tab is navigated to an unsupported URL, so we'll have to use something like chrome.webRequest.onCompleted.addListener(checkTab, { tabId, types: ['main_frame'] }) but the platform doesn't allow us to register a targeted listener just for this tab id that wakes the service worker, so we have to keep it alive for observation. In other words the platform itself forces us to use inefficient patterns, which means that the problem of wasteful restarts is gonna be around for a long time and needs to be addressed properly by Chromium team, i.e. stop stigmatizing the long-running background scripts without substantiating the claims with measurements, add the necessary histograms (frequency of restarts, duration of initialization and CPU consumption), implement adaptive behavior depending on the initialization duration, frequency of restarts, and the amount of free RAM.

Addon Land

unread,
May 23, 2024, 4:59:05 PMMay 23
to woxxom, Chromium Extensions
Wow, thank you for the detailed response, very informative.  I suppose, as you said, they do have the ability to perform optimizations, they can  choose later to not unload SWs based on multiple metrics, including which specific listeners they are handling.

Reply all
Reply to author
Forward
0 new messages