shanzilla
shanzilla

Reputation: 477

How to avoid a flash of misstyled content before content scripts load in a chrome extension?

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 */
}

Upvotes: 4

Views: 751

Answers (1)

woxxom
woxxom

Reputation: 73506

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);
    }
});

Upvotes: 4

Related Questions