Google has been beta-testing their asynchronous Analytics snippet for a while now. The code has been revised several times already, but still I think it can use some more optimizations.
The latest snippet is the following:
<script>
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-X']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
How it works
In case you’re wondering what’s with the _gaq variable, this is described in the Google Analytics Asynchronous Tracking Guide™:
The
_gaqobject is what makes the asynchronous syntax possible. It acts as a queue, which is a first-in, first-out data structure that collects API calls untilga.jsis ready to execute them. To add something to the queue, use the_gaq.pushmethod.
To be more precise — initially, _gaq is just a plain JavaScript Array. The snippet then uses the native push method to add some entries to the end of the array, such as specifying your account ID. After ga.js is loaded from Google’s servers, the _gaq.push method is replaced with a custom method, which executes all entries in the _gaq array. At this point, _gaq is no longer an Array, but an [Analytics] Object, and instant execution of the tracking methods is possible.
The next part of the snippet — (function() { … })(); — injects a new script element in the document to load ga.js, where all the magic happens. This bit of code is wrapped inside a self-invoking anonymous function immediately-invoked function expression, to avoid polluting the global scope with the temporary variables created in the process.
Possible optimizations
Don’t use _gaq || []
The first line of the script reads:
var _gaq = _gaq || [];
This line is there to allow multiple Analytics snippets in the same document. It ensures that the second snippet doesn’t overwrite a _gaq defined by the first. In other words, if _gaq is already defined, the script will continue to use that variable; if not, it creates a new array instead. However, there are far better techniques to instantiate multiple trackers than just including the same snippet twice, with a different account ID.
So, unless you’re using multiple Analytics snippets in the same document using bad code, you can safely use this instead:
var _gaq = [];
One push should suffice
The example snippet adds two elements to the _gaq array, by using the push method twice.
_gaq.push(['_setAccount', 'UA-XXXXX-X']);
_gaq.push(['_trackPageview']);
While the above is probably a bit more readable, this can perfectly be written as a single push statement:
_gaq.push(['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']);
Of course, if you’re using more advanced tracking settings, you’ll need to push even more commands. You can safely do this for all of them at once.
Combined with the previous optimization, we don’t even need .push anymore:
var _gaq = [['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']];
(Note that since _gaq needs to be a global variable anyway, we could even omit var here, saving another 4 bytes. I decided not to include this optimization because it’s confusing, it looks sloppy, and it would throw a reference error in ES5 strict mode.)
Don’t set ga.type
Before inserting the dynamically created script element into the DOM, its type attribute is set to text/javascript:
var ga = document.createElement('script');
ga.type = 'text/javascript';
This is entirely unnecessary. In every single A-grade browser, script elements default to text/javascript when the type attribute is omitted. Moreover, in HTML5, the type attribute on script elements is optional. It’s safe to omit this line.
Declare all variables in one go
The snippet uses two variables inside the anonymous function: ga, which holds the dynamically generated script element; and s, which is a reference to the first script element to occur in the document.
var ga = document.createElement('script');
// …
var s = document.getElementsByTagName('script')[0];
By declaring both vars in one go and by renaming ga into g, we can get rid of some bytes:
var g = document.createElement('script'),
s = document.getElementsByTagName('script')[0];
Note: If support for Firefox < 9 and BlackBerry OS 5 is not an issue, you could just use document.scripts[0].
Create shortcut references for recurring vars
As you can see, document is used twice. By passing document as an argument to the anonymous function, we can squeeze out even more bytes:
(function(d) {
var g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
// …
})(document);
This also improves run-time performance, since it avoids scope lookups for document.
We can go even further by also passing "script" as an argument, since that string is used twice as well:
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
// …
})(document, 'script');
That’s just to save bytes though.
To comply with the “require parens around immediate invocations” option in JSLint, you’ll have to use (function() { }()); instead of (function() { })();. Personally, I agree; (fn()) makes more sense than (fn)().
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
// …
}(document, 'script'));
Alternatively, you could just declare t as a variable within the IIFE:
(function(d) {
var t = 'script',
g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
// …
}(document));
Use the protocol check only when it’s needed
The ga.js file needs to be served via the same protocol as the document it’s used in. To make sure of this, the default Analytics snippet includes a simple protocol check:
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
Instead of document.location, you could use window.location to save two bytes, or just location to save a total of 9 bytes.
Your site should run over HTTPS/TLS, redirecting requests from HTTP to the HTTPS version. Even if you don’t support HTTPS, the TLS check isn’t needed at all, and you can safely remove it — just always serve the Google Analytics script over HTTPS.
Aside from saving a few bytes, I found that omitting the protocol check and replacing it with a hardcoded string is up to two thousand times faster. There’s no reason not to change this.
ga.src = 'https://ssl.google-analytics.com/ga.js';
Heck, if you’re doing the right thing and serving your site over HTTPS, we can even remove the https: scheme so we end up with a protocol-relative URL:
ga.src = '//ssl.google-analytics.com/ga.js';
However, now that TLS is encouraged for everything and doesn’t have performance concerns, this technique is now an anti-pattern. If the asset you need is available over HTTPS, then always use the https:// asset.
Asynchronous loading
To ensure the dynamically inserted <script> element doesn’t block rendering in any browsers, it gets the async attribute. This causes ga.js to be executed asynchronously, as soon as it’s loaded.
ga.async = true;
Since async is a boolean attribute in HTML, its presence on an element is enough. As a result, we can use JavaScript to set the async property of the ga object to any truthy value.
ga.async = 1;
Another 3 bytes saved.
Taking it one step further, we could merge this together with the next line of code, where the src property is set:
ga.async = 1;
ga.src = 'https://ssl.google-analytics.com/ga.js';
Since the URL that’s set as the src is a truthy string, we could just as well have written the following:
ga.async = ga.src = 'https://ssl.google-analytics.com/ga.js';
While this is slightly less readable, it saves another two bytes. (Hat tip: Jake Archibald.)
Update: Kyle Simpson points out that all dynamically inserted <script>s default to .async=true as per the spec. Most browsers have always implemented it this way. The only exceptions are Firefox 3.6 and Opera 10–12, who execute scripts in insertion order by default, which may cause an unnecessary delay. By setting .async=true explicitly we make sure ga.js doesn’t wait for other previously loaded scripts and doesn’t block any subsequently loaded scripts. This line of code only affects Firefox 3.6. (Sadly, it doesn’t seem to make a difference in Opera). If you’re not using any other scripts, or Firefox 3.6 support is not an issue, you can safely remove it to save even more bytes. For more information, see “HTML5 script execution changes in Firefox 4”.
Update (August 2011): Now that Firefox 6 is released, it feels like the right time to just drop the .async.
Update (January 2012): Steve Souders did some additional research and found that setting .async = true also has a positive effect in OmniWeb 622 (just like in Firefox 3.6). While it’s definitely good to know, this doesn’t change anything for me. I commented on Steve’s post, explaining that setting .async = true for dynamically inserted scripts is only worthwhile if three conditions are met.
Final result
<script>
var _gaq = [['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']];
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = 'https://ssl.google-analytics.com/ga.js';
s.parentNode.insertBefore(g, s);
}(document, 'script'));
</script>
We have reduced the original snippet of 440 bytes to one of 276 bytes with better performance. Finally, we can minify this, by removing unnecessary whitespace and semicolons:
<script>var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//ssl.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'))</script>
The optimized code minifies to 239 bytes and executes faster than the original (440 bytes).
As mentioned before, if you don’t care about Firefox < 9 or BlackBerry OS 5 support, it gets even better — because then the use of document.getElementsByTagName('script') can be avoided by using the document.scripts DOM tree accessor:
<script>
var _gaq = [['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']];
(function(d) {
var g = d.createElement('script'),
s = d.scripts[0];
g.src = 'https://ssl.google-analytics.com/ga.js';
s.parentNode.insertBefore(g, s);
}(document));
</script>
That’s just 267 characters, which minify to 219 bytes.
<script>var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];(function(d){var g=d.createElement('script'),s=d.scripts[0];g.src='//ssl.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document))</script>
Of course, it’s possible to reduce the snippet’s byte size even more, for example by using placeholder arguments instead of var, or by getting rid of the immediately-invoked function expression. It’s important to watch out for global namespace pollution though. I’d say the above snippet is the perfect balance between efficiency and readability, and would make a fine replacement for the default snippet provided by Google.
Of course, this piece of code could easily be made into a generic “asynchronous script loader” snippet that works with any script URL (not just ga.js).
How to use it?
Google recommends placing the asynchronous snippet as high as possible in the document:
One of the main advantages of the asynchronous snippet is that you can position it at the top of the HTML document. This increases the likelihood that the tracking beacon will be sent before the user leaves the page. We’ve determined that on most pages, the optimal location for the asynchronous snippet is at the bottom of the
<head>section, just before the closing</head>tag.
However, you can place the snippet in the <body> as well, be it at the top, or at the bottom. If you’re already using separate .js files for other scripts, you can even include the snippet in there so it will be cached along with the other scripts. This can be a valid reason not to put the code in the <head> after all.
Update (March 2013): Universal Analytics
Google Universal Analytics is now in public beta and features a new snippet:
<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXX-Y');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
After deobfuscation, this looks like:
<!-- Google Analytics -->
<script>
(function(window, document, strScript, url, variableName, scriptElement, firstScript) {
window['GoogleAnalyticsObject'] = variableName;
window[variableName] = window[variableName] || function() {
(window[variableName].q = window[variableName].q || []).push(arguments);
};
window[variableName].l = 1 * new Date();
scriptElement = document.createElement(strScript),
firstScript = document.getElementsByTagName(strScript)[0];
scriptElement.async = 1;
scriptElement.src = url;
firstScript.parentNode.insertBefore(scriptElement, firstScript)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-XXXX-Y');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
By applying the optimizations discussed in this post, this becomes:
<script>
(function(window, document, variableName, scriptElement, firstScript) {
window['GoogleAnalyticsObject'] = variableName;
window[variableName] || (window[variableName] = function() {
(window[variableName].q = window[variableName].q || []).push(arguments);
});
window[variableName].l = +new Date;
scriptElement = document.createElement('script'),
firstScript = document.scripts[0];
scriptElement.src = 'https://www.google-analytics.com/analytics.js';
firstScript.parentNode.insertBefore(scriptElement, firstScript)
}(window, document, 'ga'));
ga('create', 'UA-XXXX-Y');
ga('send', 'pageview');
</script>
Minified, this becomes:
<script>(function(G,o,O,g,l){G.GoogleAnalyticsObject=O;G[O]||(G[O]=function(){(G[O].q=G[O].q||[]).push(arguments)});G[O].l=+new Date;g=o.createElement('script'),l=o.scripts[0];g.src='https://www.google-analytics.com/analytics.js';l.parentNode.insertBefore(g,l)}(this,document,'ga'));ga('create','UA-XXXX-Y');ga('send','pageview')</script>
Google’s original, semi-compressed version is 440 bytes. The optimized, minified version is 332 bytes and executes slightly faster. Don’t use the optimized snippet if you care about Firefox < 9 or BlackBerry OS 5 or OmniWeb 622.
Comments
Brian wrote on :
Mathias wrote on :
Nate Cavanaugh wrote on :
Mathias wrote on :
Kyle Simpson wrote on :
Mathias wrote on :
j@ubourg wrote on :
Mathias wrote on :
Rob Flaherty wrote on :
Sander Aarts wrote on :
Mathias wrote on :
Sander Aarts wrote on :
Robin Jakobsson wrote on :
Antti Kokkonen wrote on :
Martin wrote on :
Mathias wrote on :
Martin wrote on :
Amal Roy wrote on :
Wim Leers wrote on :
Mathias wrote on :
Wim Leers wrote on :
Kyle Simpson wrote on :
Mathias wrote on :
dmsr wrote on :
MrBester wrote on :
Mathias wrote on :
Master James wrote on :
Robbie wrote on :
Peter wrote on :
Mathias wrote on :
Ruben wrote on :
Mathias wrote on :
Mathias wrote on :
Samuel H. wrote on :
Devin Price wrote on :
Martin wrote on :
Rilwis wrote on :
BKH wrote on :
Mathias wrote on :
Jeff Byrnes wrote on :
Mathias wrote on :
Boomhauer wrote on :
Jeroen wrote on :
Mathias wrote on :
Denis Ryabov wrote on :
Mathias wrote on :
Trevin wrote on :
Binyamin wrote on :
JasonDavis wrote on :
Binyamin wrote on :
Mathias wrote on :
CW wrote on :
seteh wrote on :
Mathias wrote on :
Dannii wrote on :
Mathias wrote on :
Tobias wrote on :
Mathias wrote on :
Tobias wrote on :
Devin Rhode wrote on :
Mathias wrote on :
Andrew wrote on :
Pete Jones wrote on :
Mathias wrote on :
Joris van Summeren wrote on :
Mathias wrote on :
Joris van Summeren wrote on :
Joris van Summeren wrote on :
Kevin Suttle wrote on :
TangRufus wrote on :
Mathias wrote on :
Binyamin wrote on :
Ricardo Henrique wrote on :
Richard Karlos wrote on :
Valtteri wrote on :
Valtteri wrote on :
Leave a comment