4

I'm trying to create my first Chrome browser extension. It should use content scripts to manipulate the DOM on every page of the specified domain. Initially I created a style rule in style.css using only selectors that already existed in the page I was manipulating—this method worked as expected.

I then decided to extend the functionality by adding options, allowing the user to choose from 3 states which relate to 3 different style rules. I added scripts.js to set a class based on the chosen option that I would use as a selector to apply the appropriate style rule. The problem is that now I have to wait for the state to be read from chrome storage before my custom class is applied which means there's a flash of the default styles on the page before my styles take effect.

What method should I use to prevent the delay before my styles load?

manifest.json (partial)

"content_scripts": [
  {
    "js": [ "scripts.js" ],
    "css": [ "style.css" ],
    "matches": [ "https://example.com/*" ]
  }
]

scripts.js

chrome.storage.sync.get("state", function (obj) {
  var elem = document.getElementById('targetId');

  if (obj.state === 'targetState') {
    elem.className += ' myClass';
  }

});

style.css

.myClass {
  /* do something */
}
shanzilla
  • 467
  • 4
  • 16

1 Answers1

3

Stylish chrome extension solved this problem using the following steps:

  1. Cache the state in a background page script variable and message it back to the content script which asks for the data on its start.
  2. Optionally send a message to the content script from chrome.webNavigation.onCommitted which occurs before the page started loading, sometimes even before a content script runs so this method is just an additional measure. You can make it the sole method, though, by sending the same message several times using e.g. setInterval in the background page script.
  3. Use a "persistent": true background page. Arguably, it's the only method to avoid FOUC reliably in this communication scenario as non-persistent event pages need some time to load.
  4. Declare the content script to be injected at "document_start".
    When the content script executes the document is empty, no head, no body. At this point Stylish extension, its function being style injection, simply adds a style element directly under <html>.

In your case an additional step is needed:

  1. Use MutationObserver to process the page as it's being loaded (example, performance info).

manifest.json:

"background": {
    "scripts": ["background.js"]
},
"content_scripts": [
    {
        "js": ["contents.js"]
        "matches": ["<all_urls>"],
        "run_at": "document_start",
        "all_frames": true,
    }
],

Content script:

var gotData = false;

chrome.runtime.sendMessage({action: 'whatDo'}, doSomething);

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
    if (msg.action == 'doSomething') {
        doSomething(msg);
    }
});

function doSomething(msg) {
    if (gotData || !msg || !msg.data)
        return;

    gotData = true;

    new MutationObserver(onMutation).observe(document, {
        childList: true, // report added/removed nodes
        subtree: true,   // observe any descendant elements
    });

    function onMutation(mutations, observer) {
        // use the insanely fast getElementById instead of enumeration of all added nodes
        var elem = document.getElementById('targetId');
        if (!elem)
            return;
        // do something with elem
        .............
        // disconnect the observer if no longer needed
        observer.disconnect();
    }
}

Background page script:

var state;

chrome.storage.sync.get({state: true}, function(data) {
    state = data.state;
});

chrome.storage.onChanged.addListener(function(changes, namespace) {
    if (namespace == 'sync' && 'state' in changes) {
        state = changes.state.newValue;
    }
});

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
    if (msg.action == 'whatDo') {
        sendResponse({action: 'doSomething', data: state});
    }
});

chrome.webNavigation.onCommitted.addListener(function(navDetails) {
    chrome.tabs.sendMessage(
        navDetails.tabId,
        {action: 'doSomething', data: state},
        {frameId: navDetails.frameId}
    );
});

Repeated messaging, a simple example that doesn't check if the message was processed:

chrome.webNavigation.onCommitted.addListener(function(navDetails) {
    var repetitions = 10;
    var delayMs = 10;
    send();

    function send() {
        chrome.tabs.sendMessage(
            navDetails.tabId,
            {action: 'doSomething', data: state},
            {frameId: navDetails.frameId}
        );
        if (--repetitions)
            setTimeout(send, delayMs);
    }
});
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • I tried this method and it's giving me the same results as before. doSomething—or as I named it, addClasses—isn't called until after the page loads so I still have the flash. – shanzilla Feb 03 '17 at 01:06
  • I've updated the answer. Apparently you didn't use document_start and MutationObserver, which I wrongly assumed you were. – wOxxOm Feb 03 '17 at 01:26
  • I'm still seeing the flash about half the time. It seems to mostly be there on initial page load but tends to go away as I navigate between pages within the site. Definitely better than before though and I seem to be on the right track. Thanks! – shanzilla Feb 03 '17 at 01:48
  • 1
    It should be possible to remove FOUC completely by sending the message from onCommitted repeatedly (the Tampermonkey experimental "fast injection mode") using a setInterval (e.g. 10 times with 10ms interval). You may also try sending from chrome.tabs.onUpdated. Otherwise, there's probably something else going on. How do you open that page initially? – wOxxOm Feb 03 '17 at 01:55
  • I've added an example in the answer. – wOxxOm Feb 03 '17 at 03:21
  • Thanks for your help! I also changed my css to take care of some default styles that will now be in place before the js classes are applied. – shanzilla Feb 03 '17 at 09:25