<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1822615684631785&amp;ev=PageView&amp;noscript=1"/>

Getting the timing of a Gmail Chrome extension content script just right

Gmail Chrome Extension Content Script

Today, after having been in this business for more than five years, I realized that a small percentage of people who installed our GMass Chrome extension never got it to work. Why? Because our buttons never showed up for them. Why? Because the extension’s “content script” never ran. In this article, I’ll dig into the mistake I was making and how I fixed it. If you’re developing a Chrome extension for Gmail, this is a critical concept to understand.

The three timing options for content scripts

When you designate your content script in your manifest.json file, there are three timing options for when your content script should run. They are:

  • document_idle
  • document_start
  • document_end

Google explains them in detail. They recommend that most extensions use document_idle, which fires in between the time the DOM is loaded and right after the window is loaded. However, if you’re developing an extension specifically for Gmail where you’re manipulating the Gmail DOM (Document Object Model), this will get you into trouble. In fact, all three of these can cause your script never to run. A complicating factor comes into play if you’re using the awesome Inbox SDK library with your extension.

Inbox SDK recommends that you remotely load your content script via its “loader” function. This is a beautiful concept because it allows you to make major changes to your extension without having to update your package with the Chrome Web Store and then wait for approval, and then wait again for all of your users’ browsers to get the update. The downside of this approach, however, is that your script could run later than you expect. The big flaw I discovered today was that in my content script, I had all my code wrapped in a window.onload event, like so:

window.onload = function(){
GMassReady();
}

function GMassReady(){...

}

In my manifest.json, I had my content script set to run at document_end:

"content_scripts": [ {
"js": [ "inboxsdk.js", "gmass.js"],
"matches": [ "http://mail.google.com/*", "https://mail.google.com/*" ],
"run_at": "document_end"
} ],

This worked for most users, but didn’t work for some. If your browser loaded Gmail particularly fast but loaded my remote content script particularly slowly, then the main code in the content script would never run. Why? Because window.onload would never fire, as it had already fired long before the remotely loaded content script was, well…remotely loaded. So window.onload fired before the script even existed in the browser’s scope.

Now, one solution to this problem is simply to package the script as part of the extension and stop loading it remotely. Then, using the window.onload wrapper would work, because it would guarantee that the script would be available by the time window.onload ran. However, I didn’t want to do that for the reason stated above.

How Gmail loads differently

The confusion for me in reading Google’s documentation on the run_at setting is that the rules for Gmail are different. The documentation says that if you’re using document_idle, you don’t need to wait for window.onload in your content script. This is wrong. Based on this, you might think you can use document_end and then call your content script, but this won’t work well either. That’s because the way the Gmail interface loads, the document_end event doesn’t actually fire when the DOM is ready, as the documentation states. Let’s prove this.

Here’s what happens when the script is run locally using document_end, so waiting until after the DOM is supposedly ready.

"run_at": "document_end"
DOM is not ready
Just from the look of Gmail, it’s obvious that the DOM isn’t ready when the script fires after document_end.

 

query for DOM proof
To prove that the Gmail DOM isn’t ready, we’ll query for an element that I know the Gmail DOM has, the “search” box. It’s not found.

 


Gmail UI is ready
caption: To compare, this is what the Gmail UI looks like when window.onload fires. Clearly the Gmail UI is ready.

 

Gmail search box element
And as expected, after window.onload has fired, the “search” box’s element can easily be found.

Chrome extension world vs. real life

The “document_end” option corresponds to the real-life JavaScript “DOMContentLoaded” event. Let’s look at the exact definitions of each.

document_end: “after the DOM is complete, but before subresources like images and frames have loaded.”

DOMContentLoaded: “The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.”

See the similarities? So theoretically, if you are using a local content script, and you used document_start in manifest.json and wrapped your content script code inside a DOMContentLoaded event, that is the same thing as using document_end in your manifest.json and not wrapping your script inside a DOMContentLoaded event. If you’re using a remote content script, these rules go out the window because you simply don’t know how long it will take to load your script from its server. It’s possible that your script loads after the DOMContentLoaded event has already fired, and so wrapping your code in that event would cause it never to run.

So what’s the best approach?

The optimal setting, then, regardless of whether your content script is local or remote, is to set the script to document_start in manifest.json and then in your actual content script, wrap your code in an if/then condition based on whether or not window.onload has fired yet. By using document_start, it gets your script into the browser’s context as quickly as possible, and by using the  if/then window.onload logic, it allows your script to run after it’s loaded if Gmail is ready or wait until the Gmail interface is ready. You could use document_idle or document_end, but that will just delay the inevitable…your script running. As long as you’re wrapped in a window.onload if/then checking system, you’re good.

Here’s the code:

if (document.readyState === "complete")
{
GMassReady();
}
else
{
window['onload'] = function () {
GMassReady();
}
}

function GMassReady(){...

Note: You might notice that my extension still uses document_end. Why? Because I haven’t updated my Chrome extension package yet in the Chrome Web Store. A few years ago, it was a simple process. You just uploaded a new package, and the Chrome Web Store updated — then worldwide, everyone’s browsers updated over the next couple of days. Now there’s a strict human review process before changes go live. Since I was able to fix my issue by altering my content script to include the new window.onload logic, that suffices for now.

What do other Gmail Chrome extensions do?

It turns out none of the other major Gmail extensions I tested use the default document_idle setting. As with many things in life, it seems I was the last to know about this. Let’s look at the manifest.json files of a couple of other extensions.

Mailtrack

Here’s the relevant portion of manifest.json:

content_scripts": [
        {
            "matches": [
                "https://mail.google.com/*"
            ],
            "js": [
                "scripts/lib/intercom-snippet.js",
                "scripts/lib/snowplowSnippet.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "https://mail.google.com/*"
            ],
            "js": [
                "scripts/gmail.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "https://mail.google.com/*"
            ],
            "js": [
                "scripts/bundles/gmail.start.bundle.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "https://mail.google.com/*"
            ],
            "css": [
                "styles/style.css"
            ],
            "run_at": "document_end"
        },
        {
            "matches": [
                "https://mail.google.com/*"
            ],
            "js": [
                "scripts/bundles/gmail.end.bundle.js"
            ],
            "run_at": "document_end"
        },
        {
            "matches": [
                "*://mailtrack.io/*/dashboard/welcome*",
                "*://mailtrack.io/*/dashboard/reauthorized*",
                "*://mailtrack.io/*/dashboard/install-success*",
                "*://mailtrack.io/*/dashboard/payment/teams/success*"
            ],
            "js": [
                "scripts/bundles/setup.bundle.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "*://mailtrack.io/*"
            ],
            "js": [
                "scripts/bundles/dashboard.bundle.js"
            ],
            "run_at": "document_end"
        }
    ],

I haven’t dug into each content script here, but we can see that none of them use Google’s recommendation of document_idle.

Mixmax

Here’s a pertinent snippet of manifest.json:

"content_scripts": [
        {
            "matches": [
                "*://*.mixmax.com/*"
            ],
            "exclude_matches": [
                "*://*.mixmax.com/public/analyticsbridge.html"
            ],
            "js": [
                "src/content/globals.js",
                "src/assets/lib/raven-3.3.0.js",
                "src/assets/lib/Environment.js",
                "src/assets/lib/raven-config.js",
                "src/assets/lib/error.js",
                "src/content/ExtensionMessageBus.js"
            ],
            "all_frames": true,
            "run_at": "document_start"
        },
        {
            "matches": [
                "*://mail.google.com/*"
            ],
            "js": [
                "src/content/unblock.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "*://mail.google.com/*",
                "*://*.force.com/*",
                "*://*.salesforce.com/*"
            ],
            "js": [
                "src/content/globals.js"
            ],
            "run_at": "document_start"
        },
        {
            "matches": [
                "*://mail.google.com/*"
            ],
            "js": [
                "src/content/pageInterop.js"
            ],
            "run_at": "document_end"
        },
        {
            "matches": [
                "*://mail.google.com/*",
                "*://*.force.com/*",
                "*://*.salesforce.com/*"
            ],
            "js": [
                "src/assets/lib/raven-3.3.0.js",
                "src/assets/lib/Environment.js",
                "src/assets/lib/raven-config.js",
                "src/assets/lib/error.js",
                "src/content/ExtensionMessageBus.js",
                "src/content/app.js"
            ],
            "run_at": "document_end"
        },
        {
            "matches": [
                "*://www.linkedin.com/sales/widget/*"
            ],
            "js": [
                "src/content/globals.js"
            ],
            "all_frames": true,
            "run_at": "document_start"
        },
        {
            "matches": [
                "*://www.linkedin.com/sales/widget/*"
            ],
            "js": [
                "src/assets/lib/raven-3.3.0.js",
                "src/assets/lib/Environment.js",
                "src/assets/lib/raven-config.js",
                "src/assets/lib/error.js",
                "src/content/app.js"
            ],
            "all_frames": true,
            "run_at": "document_end"
        },
        {
            "matches": [
                "&lt;all_urls&gt;"
            ],
            "exclude_matches": [
                "*://mail.google.com/*"
            ],
            "all_frames": true,
            "js": [
                "src/content/mailTo.js",
                "src/content/callTo.js"
            ],
            "run_at": "document_idle"
        }
    ],

Again I haven’t dug into each one, but none of them use document_idle except the last one, which is the one script that does not run inside Gmail.

In Conclusion…

Developing an extension for Gmail is different than for other sites. We love Inbox SDK, and we love the ability to load content scripts remotely, but it’s important to get the timing right. Set your manifest.json to use document_start and wrap your content script code in an if/then that checks for the window.load event.

Ready to transform Gmail into an email marketing/cold email/mail merge tool?


Only GMass packs every email app into one tool — and brings it all into Gmail for you. Better emails. Tons of power. Easy to use.


TRY GMASS FOR FREE

Download Chrome extension - 30 second install!
No credit card required
Love what you're reading? Get the latest email strategy and tips & stay in touch.
   


1 Comment
  1. I’m struggling with this problem for a couple of days. I had to put a setTimeout() to “patch” the problem temporarily.

    Great article, very helpful.

Leave a Reply to Patrick Dufour Cancel reply

Your email address will not be published. Required fields are marked *

Transform your Gmail account into an email marketing powerhouse

GMass is easy to learn and easy to use — but brings unbelievable email power into Gmail

Try GMass for free Then check out the quickstart guide to send your first mail merge email in minutes!

GMass

Share This