254
votes

I'm on a quest to reach 100/100 on PageSpeed and i'm almost there. I'm trying to find a good solution to cache Google Analytics.

Here is the message I get:

Leverage browser caching Setting an expiry date or a maximum age in the HTTP headers for static resources instructs the browser to load previously downloaded resources from local disk rather than over the network. Leverage browser caching for the following cacheable resources: http://www.google-analytics.com/analytics.js (2 hours)

The only solution i've found was from 2012 and I do not think it is a good solution. Essentially you copy the GA code and host it yourself. You then run a cron job to recheck Google once a day to grab the latest GA code and replace it.

http://diywpblog.com/leverage-browser-cache-optimize-google-analytics/

What else can I do to reach 100/100 while also using Google Analytics?

Thank you.

20
I used the cron method, Without cron usage ( loads and caches onload. i can share php code if you want ). And i got fixed my GA fixing suggestion. But little problem left there: I left "Cache-Control: max-age=604800" header. Which is much higher then 5 minutes cache.Roman Losev
Is that really a good idea, though? Caching this file on your server means the browser will have to re-download it instead of re-using the one it has already cached by visiting other sites using Google Analytics. So it may actually slightly slow down your visitors.s427

20 Answers

254
votes

Well, if Google is cheating on you, you can cheat Google back:

This is the user-agent for pageSpeed:

“Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.8 (KHTML, like Gecko; Google Page Speed Insights) Chrome/19.0.1084.36 Safari/536.8”

You can insert a conditional to avoid serving the analytics script to PageSpeed:

<?php if (!isset($_SERVER['HTTP_USER_AGENT']) || stripos($_SERVER['HTTP_USER_AGENT'], 'Speed Insights') === false): ?>
// your analytics code here
<?php endif; ?>

Obviously, it won't make any real improvement, but if your only concern is getting a 100/100 score this will do it.

41
votes

There's a subset of Google Analytics js library called ga-lite that you can cache however you want.

The library uses Google Analytics' public REST API to send the user tracking data to Google. You can read more from the blog post about ga-lite.

Disclaimer: I am the author of this library. I struggled with this specific problem and the best result I found was to implement this solution.

23
votes

Here is a really simple solution using JS, for basic GA tracking, which will also work for edge caches/proxies (this was converted from a comment):

if(navigator.userAgent.indexOf("Speed Insights") == -1) {
  (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','https://www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-XXXXXXXXX-X', 'auto');
  ga('send', 'pageview');
}

Note: This is the default GA script. You may have other ga() calls, and if so, you would need to always check the user agent before calling ga(), otherwise it may error out.

17
votes

I wouldn't worry about it. Don't put it on your own server, it sounds like this is an issue with Google, but as good as it gets. Putting the file on your own server will create many new problems.

They probably need the file to get called every time rather than getting it from the client's cache, since that way you wouldn't count the visits.

If you have a problem to feel fine with that, run the Google insights URL on Google insights itself, have a laugh, relax and get on with your work.

10
votes

In the Google docs, they've identified a pagespeed filter that will load the script asynchronously:

ModPagespeedEnableFilters make_google_analytics_async

You can find the documentation here: https://developers.google.com/speed/pagespeed/module/filter-make-google-analytics-async

One thing to highlight is that the filter is considered high risk. From the docs:

The make_google_analytics_async filter is experimental and has not had extensive real-world testing. One case where a rewrite would cause errors is if the filter misses calls to Google Analytics methods that return values. If such methods are found, the rewrite is skipped. However, the disqualifying methods will be missed if they come before the load, are in attributes such as "onclick", or if they are in external resources. Those cases are expected to be rare.

6
votes

You can try to host the analytics.js locally and update it's contents with a caching script or manually.

The js file is updated only few times a year and if you don't need any new tracking features update it manually.

https://developers.google.com/analytics/devguides/collection/analyticsjs/changelog

6
votes

store localy analytics.js, but it is not recommended by google: https://support.google.com/analytics/answer/1032389?hl=en

it is not recommended cause google can update script when they want, so just do a script that download analytics javascript each week and you will not have trouble !

By the way this solution prevent adblock from blocking google analytics scripts

6
votes

varvy.com (100/100 Google page speed insight) loads google analitycs code only if user make a scroll of the page:

var fired = false;

window.addEventListener("scroll", function(){
    if ((document.documentElement.scrollTop != 0 && fired === false) || (document.body.scrollTop != 0 && fired === false)) {

        (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-XXXXXXXX-X', 'auto');
        ga('send', 'pageview');

        fired = true;
    }
}, true);
6
votes

You can proxy the google analytics script via your own server, save it locally and auto update the file every hour to make sure it's always latest version from google.

I've done this on a couple of sites now and all is working fine.

Google Analytics Proxy Route in NodeJS / MEAN Stack

This is how I implemented it on my blog that's built with the MEAN stack.

router.get('/analytics.js', function (req, res, next) {
    var fileUrl = 'http://www.google-analytics.com/analytics.js';
    var filePath = path.resolve('/content/analytics.js');

    // ensure file exists and is less than 1 hour old
    fs.stat(filePath, function (err, stats) {
        if (err) {
            // file doesn't exist so download and create it
            updateFileAndReturn();
        } else {
            // file exists so ensure it's not stale
            if (moment().diff(stats.mtime, 'minutes') > 60) {
                updateFileAndReturn();
            } else {
                returnFile();
            }
        }
    });

    // update file from remote url then send to client
    function updateFileAndReturn() {
        request(fileUrl, function (error, response, body) {
            fs.writeFileSync(filePath, body);
            returnFile();
        });
    }

    // send file to client
    function returnFile() {
        res.set('Cache-Control', 'public, max-age=' + oneWeekSeconds);
        res.sendFile(filePath);
    }
});

Google Analytics Proxy Action Method in ASP.NET MVC

This is how I implemented it on other sites built with ASP.NET MVC.

public class ProxyController : BaseController
{
    [Compress]
    public ActionResult GoogleAnalytics()
    {
        var fileUrl = "https://ssl.google-analytics.com/ga.js";
        var filePath = Server.MapPath("~/scripts/analytics.js");

        // ensure file exists 
        if (!System.IO.File.Exists(filePath))
            UpdateFile(fileUrl, filePath);

        // ensure file is less than 1 hour old
        var lastModified = System.IO.File.GetLastWriteTime(filePath);
        if((DateTime.Now - lastModified).TotalMinutes > 60)
            UpdateFile(fileUrl, filePath);

        // enable caching for 1 week for page speed score
        Response.AddHeader("Cache-Control", "max-age=604800");

        return JavaScript(System.IO.File.ReadAllText(filePath));
    }

    private void UpdateFile(string fileUrl, string filePath)
    {
        using (var response = WebRequest.Create(fileUrl).GetResponse())
        using (var dataStream = response.GetResponseStream())
        using (var reader = new StreamReader(dataStream))
        {
            var body = reader.ReadToEnd();
            System.IO.File.WriteAllText(filePath, body);
        }
    }
}

This is the CompressAttribute used by the MVC ProxyController for Gzip compression

public class CompressAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

        var encodingsAccepted = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
        if (string.IsNullOrEmpty(encodingsAccepted)) return;

        encodingsAccepted = encodingsAccepted.ToLowerInvariant();
        var response = filterContext.HttpContext.Response;

        if (encodingsAccepted.Contains("gzip"))
        {
            response.AppendHeader("Content-encoding", "gzip");
            response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
        }
        else if (encodingsAccepted.Contains("deflate"))
        {
            response.AppendHeader("Content-encoding", "deflate");
            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
        }
    }
}

Updated Google Analytics Script

On the client side I append the analytics path with the current date up to the hour so the browser won't use a cached version more than an hour old.

<!-- 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', '/analytics.js?d=' + new Date().toISOString().slice(0, 13), 'ga');
</script>
4
votes

For Nginx:

location ~ /analytics.js {
        proxy_pass https://www.google-analytics.com;
        expires 31536000s;
        proxy_set_header Pragma "public";
        proxy_set_header Cache-Control "max-age=31536000, public";
    }

Then change path https://www.google-analytics.com/analytics.js to https://yoursite.com/analytics.js

4
votes

PHP

Add this in your HTML or PHP code:

<?php if (!isset($_SERVER['HTTP_USER_AGENT']) || stripos($_SERVER['HTTP_USER_AGENT'], 'Speed Insights') === false): ?>
  <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','https://www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-PUT YOUR GOOGLE ANALYTICS ID HERE', 'auto');
    ga('send', 'pageview');
  </script>
<?php endif; ?>

JavaScript

This works fine with JavaScript:

  <script>
  if(navigator.userAgent.indexOf("Speed Insights") == -1) {
    (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','https://www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-<PUT YOUR GOOGLE ANALYTICS ID HERE>', 'auto');
    ga('send', 'pageview');
  }
  </script>

NiloVelez already said: Obviously, it won't make any real improvement, but if your only concern is getting a 100/100 score this will do it.

1
votes

try this just insert before

<script async='async' src='https://cdn.jsdelivr.net/ga-lite/latest/ga-lite.min.js'></script> <script>var galite=galite||{};galite.UA="xx-xxxxxxx-x";</script>

Please change xx-xxxxxxx-x to your code, please check to implementation here http://www.gee.web.id/2016/11/how-to-leverage-browser-caching-for-google-analitycs.html

1
votes

In 2020 Page Speed Insights user agents are: "Chrome-Lighthouse" for mobile and "Google Page Speed Insights" for desktop.

<?php if (!isset($_SERVER['HTTP_USER_AGENT']) || stripos($_SERVER['HTTP_USER_AGENT'], 'Chrome-Lighthouse') === false  || stripos($_SERVER['HTTP_USER_AGENT'], 'Google Page Speed Insights') === false): ?>
// your google analytics code and other external script you want to hide from PageSpeed Insights here
<?php endif; ?>
0
votes

Google cautions against using local copies of the analtics scripts. However if you are doing it, you will probably want to use local copies of the plugins & the debug script.

A second concern with agressive caching is that you will be getting hits from cached pages - which may have changed or have been removed from the site.

0
votes

To fix this issue you would have to download the file locally and run a cron job to keep updating. Note: this doesn't make your website any faster at all so its best to just ignore it.

For demonstration purposes however, follow this guide: http://diywpblog.com/leverage-browser-cache-optimize-google-analytics/

0
votes

This may do the trick :)

<script>
  $.ajax({
  type: "GET",
  url: "https://www.google-analytics.com/analytics.js",
  success: function(){},
  dataType: "script",
  cache: true
  });
</script>
0
votes

Depending on your use of Google Analytics data, if you want basic information (such as visits, UI interactions) you might be able to not include analytics.js at all, yet still collect data in GA.

One option may be to instead use the measurement protocol in a cached script. Google Analytics: Measurement Protocol Overview

When you set the transport method explicitly to image, you can see how GA constructs its own image beacons.

ga('set', 'transport', 'image');

https://www.google-analytics.com/r/collect
  ?v={protocol-version}
  &tid={tracking-id}
  &cid={client-id}
  &t={hit-type}
  &dl={location}

You could create your own GET or POST requests with the required payload.

However, if you require a greater level of detail it probably won't be worth your effort.

0
votes

You can set up a cloudfront distribution that has www.google-analytics.com as its origin server and set a longer expiry header in the cloudfront distribution settings. Then modify that domain in the Google snippet. This prevents the load on your own server and the need to keep updating the file in a cron job.

This is setup & forget. So you may want to add a billing alert to cloudfront in case someone "copies" your snippet and steals your bandwidth ;-)

Edit: I tried it and it's not that easy, Cloudfront passes through the Cache-Control header with no easy way to remove it

0
votes

Open https://www.google-analytics.com/analytics.js file in a new tab, copy all the code.

Now create a folder in your web directory, rename it to google-analytics.

Create a text file in the same folder and paste all the code you copied above.

Rename the file ga-local.js

Now change the URL to call your locally hosted Analytics Script file in your Google Analytics Code. It will look something like this i.e. https://domain.xyz/google-analytics/ga.js

Finally, place your NEW google analytics code into the footer of your webpage.

You are good to go. Now check your website of Google PageSpeed Insights. It will not show the warning for Leverage Browser Caching Google Analytics. And the only problem with this solution is, to regularly update the Analytics Script manually.

-13
votes

You can minify all your scripts in page, including analytics.js using:

Remember to minify the files before using it. Otherwise it will consume more processing time.