Loading values from browser.storage (StorageArea/adon settings) into content script is too slow? (creating race conditions)

In short:

  • browser.storage API is asynchronous and thus too slow.
  • You need fast values/overwrites if you want to override JS functions on websites. That’s the use case of my add-on.

Read the exact issue if you want to know what my exact problem is, but here is the abstract/generic way of what fails:

  1. I want a user-set value from browser.storage.sync in my content script. (This should be a fairly common use case.)
  2. Now you can actually use the browser.storage API in content scripts, which is great…
  3. However that is slow! As it is asynchronous…
    In my case, website JS run faster…
  4. And I wanted to overwrite a JS function of the website…
  5. But while I am requesting my own setting to decide what I should do…
    6…the website JS has already called the “non-faked” JS API, circumventing/ignoring my add-on.

So here we are: A typical race condition that results in a bug.

You may wonder, but you can get it faster…

I noticed you obviously keep the setting loaded in your background script and only load/inject it at tab load into your own script. (This injection has it’s own issues, but these are off-topic here.)

So I just try to inject the value as fast as I can (note I need to listen to tab creation events etc., so it is also not that fast…). In the end, it’s just one line of code to inject:

gettingFakedColorStatus.then((fakedColorStatus) => {
    return browser.tabs.executeScript(tab.id, {
        code: `
            // apply setting value
            fakedColorStatus = COLOR_STATUS["${fakedColorStatus.toUpperCase()}"]
            applyWantedStyle(); // call to apply CSS
        `,
        allFrames: true,
        runAt: "document_start" // run later, so we make sure COLOR_STATUS is already loaded
    });
}

gettingFakedColorStatus is an already resolved promise of storage.addons.sync (or actually, a tiny wrapper of that API, but this does not matter here), so it is fast and does not actually wait. (it should run synchronously)

And, you may wonder, but it is actually faster than requesting the same value in the content script. In my case(testing with Firefox Nightly), it was around 20ms faster.

The problem is just: The original website’s JS that accesses the API I want to overwrite is again 20-70ms faster than even this method.
So I am too slow, again…

I have not even tried using any messaging you normally use between background script and content script. This will very likely be even slower…

What to do?

So what shall I do?
Why can’t I somehow start content scripts before a website even loads or so?
Or preload some data/JS there?

This thing totally results in race conditions, so is not there a way around it?

Pretty sure this is one of the use cases https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts was intended to solve. Since you can register it before the page would be loaded and it’d be in the same scope as your main content scripts, too.

Well, actually this is how I do it already. Anyway, though, browser.storage is asynchronous and thus too slow!

And as you can see (described under “You may wonder, but you can get it faster…”) the injectJS method is even faster (usually/in my tests) than the native content script method, because the value is pre-loaded.

So yes, I already do (value then loaded here). It’s anyway too slow…

I mean, if it were only synchronous code I would need to execute, this would be no problem, but the API is asynchronous… :cry:

I don’t think you understood what I meant there. The contentScript API lets you register a content script at run time and you can use a plain JS string like with executeScript. Thus it mixes the best of both worlds.

I had a very similar issue, I needed some settings but I needed them synchronously in the DOMContentLoaded event.

So what do you do when you can’t use asynchronous API? You use synchronous API :smiley:

But this is far from perfect, mostly because:

  1. you can store only strings, so you have to stringify everything
  2. it can get cleared with history
  3. it’s specific for the origin, so you need to set it for each page
  4. setting the value may be the biggest challenge - unless you are ok that the bug may appear on first load only :slight_smile:

So maybe I’m just brainstorming here… but maybe not.

Ah thanks for the explanation. That makes sense.

And I can obviously register it for all tabs arrgh… Obviously. Yes, that’s the solution.

given I (hopefully) can unregister the thing again… :thinking:

Oh yeah, that’s a good idea too. But as you’ve mentioned, there are a lot of caveats…

I did not get what you mean with 4., but I would add another point:
5. it’s hard to keep these values in sync then with the browser.storage API, especially if you want to use browser.storage.sync, which means the value could change at any time… :fearful:

I don’t think you can, though you could use versioning or similar to make the old one obsolete, but that could turn into a lot of dynamic code getting injected in longer sessions etc.

Actually you can do so. When you run browser.contentScripts.register you get an object, where you can call unregister.

So this here is completly solved. :smile:

1 Like