03 December 2009

How tracking scripts affect page loads… can tracking scripts kill my web app?

This post explains script blocking, and then shows how to safely setup a tracking script or any external script, such as Google Analytics or Quantcast, to not block page loads or other javascript handlers on your site.

The basics of scripts and blocking

  • HTML content that appears after a <script> tag will not display until the script is done downloading, parsing, and executing (blocking example)
  • Many javascript apps wait for the DOMContentLoaded event or an equivalent document ready event to apply functionality to the page
  • <script> tags prevent the document ready event from happening until they are done downloading, parsing, and executing

If these points make sense, skip forward to tracking scripts.

Script tags block

When a browser renders a web page it reads the HTML from top to bottom. When it encounters a <script> tag—maybe in the <head>, maybe in the <body>—it will halt from rendering the rest of the page, i.e. nothing else will show up, until the script has finished downloading, parsing, and executing.

Later scripts get blocked since a script can modify the page, add additional scripts, or add new variables to the window object. Some modern browsers will download multiple scripts in parallel (ie8, safari4) and future versions of browsers are expected to do this too, but, for the reasons just mentioned, scripts will still block the execution of later scripts and page content that is after a script will not be displayed until the script has finished.

Capturing the page load event

Many scripts need to wait for the page to load before they run, so they can do something simple like assigning event handlers to elements on the page or something more complex like paginating a long list (see jQuery UI or YUI). This can be done by assigning a function to run on window.load, but the window load event does not fire until the entire page has loaded—including images, flash, and applets. This means that users will be able to see and interact with the page, but not use your javascript enhancements until all the images have downloaded.

Mozilla and Opera fire a DOMContentLoaded event once the structure of the document has loaded and scripts have executed, but before the images, flash, and applets have loaded. By assigning a page load function to this event handler, all your javascript enhancements can be applied right when the page appears and often appear instantaneously to the user. It is important to realize that the DOMContentLoaded event will not fire until all scripts have downloaded and executed (more on this later).

Many javascript libraries offer a cross-browser way to attach a function to the equivalent of the DOMContentLoaded event. Here it is in jQuery and Prototype:

// jQuery
$(document).ready(function() { }) // or just $(function() { })

// Prototype
document.observe("dom:loaded", function() { });

Many developers rely heavily on these types of functions, especially for javascript/AJAX heavy web apps, and with this in mind, we can finally move on to the topic of tracking scripts.

Tracking Scripts

Tracking scripts are useful and extremely common. You can add as many of them as you want onto a web page. The most common tracking scripts are Google Analytics—which shows usage patterns to a site’s owner—and Quantcast—which can be used to broadcast how many people use your site and some characteristics about them. Another is Chartbeat—which emphasizes showing you analytics in real-time.

When you add a tracking script to your site, you are adding an additional script to your pages that will follow the same blocking rules as any script. This can be dangerous, because someone else’s server is hosting that script and if your servers are working fine, but something is wrong with their servers, it can affect page loads on your site.

Here is an example of the Google Analytics tracking script code. Notice there are 2 script blocks, the first adds the ga.js script to the page and downloads it (remember that the 2nd script is blocked from executing until this one has finished), and the second calls the tracker function with your tracking code.

<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("_your_tracking_code_here_");
pageTracker._trackPageview();
} catch(err) {}
</script>

Tracking scripts are designed so you can download them once and have them cached safely in your browser for future page loads on your site and even better—for page loads on any other sites that use the same type of tracking scripts. Listed below are different ways to configure a tracking script, some are good, some are bad, and some are downright ugly. Each case looks at what happens when the tracking script is not in your browser cache and takes a REALLY long to download.

Script in the <head>

A slow script here will prevent anything from appearing on your pages until the script has finished (BOOM Headshot!). Hopefully, you’ve at least put the <title> above the script, so that will appear in the browser.

Newsweek.com had Quantcast configured this way when this blog post was written.

Script at the top of the <body>

A slow script here will have the same affect as putting it in the <head>. No actual content of the page will appear until this script has finished.

Macworld.com, evite.com, and popsugar.com all had Quantcast configured this way when this post was written.

Script at the bottom of the <body>

This is the configuration recommended by tracking script providers.

A slow script here—right before you close the body tags—will allow all of the content of the page to successfully render, but any document ready or DOMContentLoaded event handlers will not fire until the script has finished. This can be fine if your site doesn’t rely heavily on javascript to modify content on the page or bring advanced functionality to page (e.g., this type of configuration is fine for this blog).

However, if your site is a javascript heavy webapp, the initial content of the page will have loaded, but none of your javascript handlers will work yet. This can be a terrible experience if the script download fails for your users who will get annoyed, leave, and never come back <flashlight>FORRREEEVVVEEER</flashlight> (please excuse my attempted Sandlot reference).

This exact problem happened to a major media site a few years ago when some problem at Google was causing the Google Analytics script to not load and their entire site would not function for a period of hours. In fact, at Hunch, we noticed just a few weeks ago that Quantcast was having some problems serving content for about an hour and half (otherwise they’ve been good).

Script inserted after a load event

This technique is designed for javascript heavy websites. The tracking script code is inserted dynamically onto the page after a load event. Here is some sample code using jQuery:

<script type="text/javascript">
  $(window).load(function() {
    var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
    $.ajax({
        url: gaJsHost+'google-analytics.com/ga.js', type: 'get', dataType: 'script', cache: true,
        success:function() { try { _gat._getTracker("_your_tracking_code_here_")._trackPageview(); } catch(err) {} }
    });
  })
</script>

Some things to be aware about:

  • window.load is not triggered until all images have finished downloading—so be careful with this if you have large, slow-loading images on your site
  • the order in which functions are attached to events is not guaranteed to be the order in which they are executed—that’s why this is using window.load instead of document.ready
  • this doesn’t work in Safari 2 or older with the current jQuery (1.3.2)—the ajax success function doesn’t get called

Google Analytics asynchronous tracking code

When I was writing this post, Google released their Google Analytics asynchronous tracking code. It’s a pretty slick approach that creates a script object dynamically and sets the async attribute to true. To configure it, you pass commands as strings into an array, which allows actions to queue up until the script loads and then run immediately once the script has loaded (see the link for specifics).

In most up-to-date versions of browsers (firefox 3.6, chrome, and safari 4) and ie6+ this code does not block the DOMContentLoaded event (or html). However, this event does get blocked in firefox 3.5 and Opera. Regardless this is pretty awesome for google analytics, and I highly recommend using it over the regular approach. Unfortunately this approach requires work by the script provider, so we’ll see if other tracking scripts, widgets, etc. take this approach. UPDATE: KISSmetrics added asynchronous support—finding more cases will be left as an exercise for the interested reader.

Host the script yourself

I forgot to mention this in my original post, but some tracking script services let you host the javascript file yourself, e.g., KISSmetrics lets you do this (if you don’t want to use their asynchronous script). If you do this, then the only time the script will get served slowly is when your site is already being slow, which means you don’t have to worry about the main point of this post—preventing a tracking script from slowing down your page loads, when everything else is fast. I think this is a great move for a big site with lots of resources since it can afford to serve the content quickly and not worry about loading something from an external server.

Conclusion

Scripts block content from loading on a web page. Prevent potential poor page loads on your site by properly configuring your tracking scripts either at the bottom of the <body> or—if you’re running a javascript heavy web app—in a window.load handler. Even popular sites sometimes get this wrong.

A slight reality check, the likelihood of scripts from a company like Quantcast or Google not loading is extremely low, but the Internet is never 100% trustworthy, so it’s nice to have things setup properly for the inevitable moment when things break. Fortunately with Google, you can now use their Google Analytics asynchronous tracking code.

Also, related to scripts and blocking, I just saw this in a tweet from @rr: LABjs: Loading And Blocking JavaScript. Looks like a pretty cool idea for controlling non-blocking of scripts, click over to the documentation part to see how it works.

UPDATE: I added info about the google analytics asynchronous tracking code

UPDATE 2: Here’s an example of this happening on a real site. I was on my favorite subway directions website and it was being bogged down by an advertisement. The javascript for their ad widget was not getting served, which blocked the main content on the page from appearing for 3 minutes and 45 seconds! Click through to see the full size images.

Blocked page load

Blocked page - finished   Blocked page - firebug net screen

UPDATE 3: I added the “Host the script yourself” section and mentioned KISSmetrics

Comments (12)

1. Florent Peyre wrote:

This is a great post! Thanks for it. I'm wondering if you've done the same analysis for the other type of categories of scripts and pixels? Like BT, Ad exchanges scripts, Social Networks scripts (FB connect etc.)? Thanks. Florent

Posted on 7 December 2009 at 12:12 PM  |  permalink

2. peter wrote:

I haven’t looked into those much in the context of this post, but if you’re linking to an external script it will follow the same blocking rules of any script. However, these other categories of scripts are a bit different from tracking scripts, since they’re often bringing functionality to the user.

If it’s something that’s providing a crucial feature to your users, maybe it’s fine having them wherever the script provider suggests. While the script can potentially block entire page loads, it should normally download and cache very well (99.9something% of the time) and be unnoticeable to user.

If you want to be more careful and put some more effort into it, you can do something incremental, like showing the links for your facebook connect as disabled and then enabling them once the script has executed. Or depending on how it works have them load with display:none; and then unhide them when the script has run.

If you’re inserting an iframe and not a script, iframes do not block the rest of the page and they do not block the DOMContentLoaded event. However, I think the window.load event will not fire until iframes have finished loading.

Basically, figure out what your priorities are with these scripts and configure them accordingly. For example, tracking scripts are not as important as the user having a good experience and it’s not the end of the world if a marginal percentage of page loads do not get recorded in your analytics. That’s why this post suggests to prioritize getting the page ready for the user and then running tracking scripts.

Posted on 7 December 2009 at 1:12 PM  |  permalink

3. Florent Peyre wrote:

Peter, Thanks a lot for the answer, this was very helpful! Florent

Posted on 9 December 2009 at 2:12 AM  |  permalink

4. Buid wrote:

Interesting explanation. Does it mean the script is the first one browser scan befor the content? Thank you.

Posted on 26 December 2009 at 5:12 PM  |  permalink

5. peter wrote:

Hey Buid, I’m not sure if I follow your question, but when a page loads, the content is read in the order that it was placed in the html. So, if a script is encountered, the rest of the page (anything that appears after the script) must wait until that script finishes. Does that clarify your question?

Posted on 28 December 2009 at 9:12 AM  |  permalink

6. Michael wrote:

Probably makes sense to mention and link to the Google Analytics asynchronous loading code which I believe acheives what you are aiming for but doesn't require the overhead and additional downloads of jQuery.

http://code.google.com/apis/analytics/docs/tracking/asyncTracking.html

Posted on 4 February 2010 at 1:02 PM  |  permalink

7. peter wrote:

Michael, thanks for the comment, I’ll update my post with this when I get a chance. The Google Analytics asynchronous tracking code is pretty cool. Its technique is good to add to this discussion on scripts and blocking. It mostly solves this problem for google analytics, but not so much for other tracking/external scripts.

I just tested out loading a slow script on a page, using their method of setting the async attribute to true, and I found some interesting things. Turns out that firefox pre 3.6 (tested on a mac and pc) was waiting for the “async” script to finish loading before firing a DOMContentLoaded event, and the same for Opera 10.10 (tested on a mac). Also, ie 6, 7, and 8 will all fire the window.onload event before the script finishes loading, contrary to pretty much all other browsers, which will wait for the script to load.

tl;dr - all up-to-date versions of modern browsers as well as ie 6, 7, and 8 will properly download the asynchronous google analytics code asynchronously.

Posted on 4 February 2010 at 4:02 PM  |  permalink

8. Brian wrote:

@peter: Some browsers don't currently provide a way for scripts to load asynchronously without blocking window.onload. So, you have to make a choice. Either you take the chance that some browsers will load your page slower or you can put your code behind window.onload and run the risk of losing analytics data. In the end, I think you'll see that ga.js loads a lot faster than the other elements on your page. It's probably the least of your worries.

Posted on 4 February 2010 at 4:02 PM  |  permalink

9. peter wrote:

@brian - on the topic of scripts blocking window.onload being an issue… the relevant bad scenario that I was referring to is when a page has important js handlers on the “DOMContentLoaded” event—this is the one you don’t want to block. With the google analytics asynchronous tracking scripts approach, the DOMContentLoaded event doesn’t get blocked in ie6+ or up-to-date versions of ff, safari, or chrome (but it does in Opera). The other approaches I mentioned (with the thumbs up) can be used too, just as you said at the end of your comment.

However, if you have a massive production website that runs javascript on the DOMContentLoaded event, which is necessary for the user experience, then you should be aware of this potential issue. While it is really unlikely to happen (maybe more likely with some scripts than ga.js), it is really bad if it does happen. Furthermore, there’s no reason for newsweek.com to have quantcast in their document head :)

Posted on 4 February 2010 at 8:02 PM  |  permalink

10. Salman wrote:

Regarding the "Script inserted after a load event" approach, I was advised not to use it for (Google) analytics scripts; it would cause the analytics to record the incorrect information or no information at all. If for example your page takes about 10 seconds to fire window.load then the "time spent on website" recorded by the script will be lower than expected. If user moves away from your page before that time then the script will not record this which affects the "bounce rate".

I am wondering if it is OK to load such scripts on document.ready, since you can manipulate the DOM and inject script tags at that time. I wonder if you've already written about this.

Posted on 2 November 2011 at 11:11 AM  |  permalink

11. peter wrote:

Salman, check out the section titled: “Google Analytics asynchronous tracking code”

Posted on 2 November 2011 at 4:11 PM  |  permalink

12. lynn wrote:

this is stupid.

Posted on 23 January 2012 at 10:01 AM  |  permalink

Peter Coles

Peter Coles

is a software engineer who lives in NYC, works at Ringly, and blogs here.
More about Peter »

github · soundcloud · @lethys · rss

It’s time to get big money out of politics. Join the kick-started campaign to put government back in the hands of the people. Pledge mayday.us now