Part 1: Road to PageSpeed 100/100: Load Analytics.js from your own server

  • 26 April 2017 - Umbraco CMS - Internet marketing
  • Leestijd: 6 minutes
Daniël Knippers Back end developer

Check your website

To have it check your site, simply put in the sites URL and sit back. Google’s PageSpeed Insights will analyze the website and will point out anything that negatively impacts the site’s loading and/or rendering performance. Among other things, the tool will look for slowdowns like unnecessarily large images, render-blocking JavaScript or CSS, unminified JavaScript/CSS/HTML, slow server response times, and inefficient browser caching instructions. It yields a score between 0 (terribly slow) and 100 (blazingly fast). In fact, it will compute 2 such scores, one for desktop and one for mobile. Of course, we aim for 100/100 on both – ambition is a good thing, right?

The good news is that the majority of issues can be fixed fairly quickly by following the recommendations presented by the PageSpeed tool, assuming you have control over both the website’s source code and the webserver itself. Both are generally required to fix all issues. However, most of the optimizations can actually be performed in the website code itself. Such optimizations include decreasing server response time by caching slow queries (or better yet, don’t write slow queries),  making sure render-blocking JavaScript is moved out of the <head> or is loaded asynchronously loaded, and minifying static resources such as JavaScript and CSS, and even the HTML itself. Webserver configuration might have to be adjusted to enable features like gzip compression for all HTTP requests and headers instructing the browser to cache static resources.

Your own webserver

After going through all the necessary adjustments, there is one issue that kept popping up in our PageSpeed reports and it is caused by Google itself. Gotta love the irony right? It turns out that the caching header for Google’s analytics.js file is not optimally set according to PageSpeed.


Quote start
Google Analytics is violating Google's PageSpeed rules
Quote stop

Google Analytics

It turns out that the webserver that serves analytics.js sets the Cache-Control header to “public, max-age=7200”. Max age is specified in seconds, which indeed amounts to 2 hours (7200/60/60). Unfortunately, we have no control over Google’s webserver so we cannot influence this header if we continue to load analytics.js from Google’s server directly. This prevents us from reaching a perfect 100/100 score, assuming we can reduce the list of issues to just this issue in the first place. In order to adjust the Cache-Control header to something more sensible (e.g., a month or even a year) we would have to serve analytics.js from a webserver under our control.

There is nothing particularly magical about analytics.js that prevents us from serving it from our own website, rather than Google’s server. It’s just a JavaScript file, no different from libraries like jQuery which we routinely serve from our own webserver, bundled with our own JavaScript. The only difference is that Google can update the analytics.js file at any time to add new features or fix bugs, and we would then have to update our local copy as well. We will get to this later. First, let’s look at the way Google Analytics is generally loaded; using a “JavaScript tracking snippet" provided by Google, to be placed in the <head> section of every page:

<!-- Google Analytics -->
(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','','ga'); 

ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
<!-- End Google Analytics -->

Understanding the code

The first step to modifying any code is to understand it and be aware of the impact of any potential changes. To that end, let’s first have a closer look what’s actually going on here. Primarily, we will look at the “isogram”-function, pretty printed now, and what exactly happens with analytics.js after it has been loaded.

  • (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', '', 'ga');
  • ga('create', 'UA-XXXXX-Y', 'auto');
  • ga('send', 'pageview');

Google Analytics setup function

Lines 2 through 5 create 2 properties on the window object. One property called “GoogleAnalyticsObject”, which will point to the property name of the Google Analytics function. Since this is Universal Analytics, that function is called “ga”. The second property created on window is called “ga”, and will later become the actual Google Analytics function. For now, it is only a tiny function which we will refer to as a “setup function”. This function contains the current timestamp as a property “l”. 1 * new Date() is a trick to force a Date object into a timestamp, a shorthand for new Date().getTime(). This can also be achieved using +new Date (note that JavaScript allows the omission of parentheses on calls to constructors without parameters).

Additionally, when the setup function is executed it will create a property “q” on if it is not there yet, and pushes the arguments with which the function was invoked onto that Array. This is used to “remember” any commands to be executed later, when the ga function has fully initialized. This behavior is used immediately on lines 12 and 13. Those 2 lines are not actually doing anything yet, unless Google Analytics was already fully initialized. This will generally not be the case, since the <script> tag that loads analytics.js was created right above the line and is asynchronously loaded. Because of this, lines 12 and 13 will execute the setup function which will then store the arguments of the function call in an Array. These will later be executed by Google Analytics. After both calls have happened, the setup function looks like this. The commands are stored in inside, a temporary Array which will be removed once Analytics is initializing, but not before the commands inside are executed.

All this setup code is essential to Google Analytics and we will leave all of it there, mostly untouched. Before we dive into our adjustments, let’s look at the remainder of the snippet.

In lines 6 through 10, a <script async src=""></script> is created through JavaScript which asynchronously loads the Google Analytics code. The script is inserted before the first script on the page. The code that will be loaded will then modify the setup function to become the actual Google Analytics function. It will then also execute the stored commands from earlier – the “send” and “create” commands.

Modify the script

Now that we understand what’s happening, we can modify the script to load analytics.js from somewhere else rather than Google’s server, as it really does not matter where it is coming from. There is also no real reason to create the <script> tag using JavaScript, or even to create a separate script tag for it at all. This means we can cut down the JavaScript tracking snippet quite a bit, and also have some fun with the argument names just like Google did. The changed snippet now looks like this:

  • <script async src="/scripts/analytics.js"></script>
  • <script>
  • (function(per, plex) {
  • per['GoogleAnalyticsObject'] = plex;
  • per[plex] = per[plex] || function() {
  • (per[plex].q = per[plex].q || []).push(arguments)
  • }, per[plex].l = 1 * new Date();
  • })(window, 'ga');
  • ga('create', 'UA-XXXXX-Y', 'auto');
  • ga('send', 'pageview');
  • </script>

All the code responsible for creating the <script> tag was removed. We will simply declare the element right inside the HTML without JavaScript. In addition, we also snuck our company name into the arguments because, well, why not? This still executes the setup logic required for Google Analytics, but now the analytics.js file will be loaded from our webserver rather than Google’s. Alternatively, we could have included the analytics.js file in a Bundle file alongside other JavaScript. 

Note that this approach can also be used to load any plugins such as ‘displayfeatures’ and ‘linkid’ from your own webserver rather than Google’s, as those too will generate a message in PageSpeed. Simply specify the local path in the ga('require') command. For example, to load linkid.js from the local /scripts/ directory you can use this syntax:

ga('require', 'linkid', '/scripts/linkid.js');

Bring in the champagne!

With this setup and with a webserver configured to not set such a short max-age for the Cache-Control header, PageSpeed will no longer see analytics.js as a performance issue, allowing us to reach 100/100!

But let’s not celebrate too early. One glaring issue with this approach is the lack of updates from Google. Although analytics.js does not appear to change a lot (Google’s server currently sends a Last-Modified header with content “Wed, 28 Sep 2016 20:19:01 GMT”, which is more than 5 months ago), it would be ill-advised to implement this approach without any mechanism in place (either manual or automatic) to check for updates regularly. Therefore, we have scheduled a job that compares our saved copy of analytics.js with the latest version from Google once per day and updates it if necessary. This way, we make sure we are running the latest version of Analytics while still passing the PageSpeed rules regarding caching.

More tips for PageSpeed 100/100

Google gives you a score on a scale of 0 to 100, of course 100 being the perfect score. Unreachable? Certainly not! For this has been realized. Find out soon in Part 2 how this has been achieved!