Journal

Back

How to achieve 100/100 on Google PageSpeed Insights 4th July 2018

Webpage load times have always been an important factor, at the very least simply from a user experience point of view, but nowadays search engines are putting far more weight on performance. Modern websites rely heavily on scripting languages such as javascript and often implement technologies such as canvas or CSS animations, which presents us with a paradox — we want all of this without paying the price on load times.

We are going to assume that readers of this post have already minified their code including all scripts and stylesheets. At Barn + Co, we combine all our essential assets into one single stylesheet and one single javascript file to reduce the number of assets and 'round trips' to the server ande often use inline SVG files to save yet more trips. When using Craft CMS the entire HTML output of the page is also minified using the Minify plugin.

Secondly, we are going assume that all images are being compressed accordingly, although it is worth noting that simply exporting a JPG at 80% quality is not enough in itself. There are always further optimisations to be made such as removing EXIF data from the file, and this can be achieved through online tools such as TinyJPG or desktop tools such as ImageOptim. Both tools work for both JPGs and PNGs with the latter also handling SVGs. The additional savings are evident in ImageOptim when turning off lossy optimisations as there can still be 5-30% file size reductions.

So you've done all the above steps and made sure your server response times are as short as possible, but you're still struggling to get the 'holy grail' Google PageSpeed Insights score of 100%. What next?

There are some quick fixes that can be achieved using server configuration files such as .htaccess for Apache and web.config for Windows servers. They are used to enable compression settings as well as to leverage browsing caching, two very common errors in Google PageSpeed Insights. Below are some example configurations.

.htaccess

# Enable Compression
<ifmodule mod_deflate.c="">
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE application/rss+xml
    AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
    AddOutputFilterByType DEFLATE application/x-font
    AddOutputFilterByType DEFLATE application/x-font-opentype
    AddOutputFilterByType DEFLATE application/x-font-otf
    AddOutputFilterByType DEFLATE application/x-font-truetype
    AddOutputFilterByType DEFLATE application/x-font-ttf
    AddOutputFilterByType DEFLATE application/x-javascript
    AddOutputFilterByType DEFLATE application/xhtml+xml
    AddOutputFilterByType DEFLATE application/xml
    AddOutputFilterByType DEFLATE font/opentype
    AddOutputFilterByType DEFLATE font/otf
    AddOutputFilterByType DEFLATE font/ttf
    AddOutputFilterByType DEFLATE image/svg+xml
    AddOutputFilterByType DEFLATE image/x-icon
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE text/html
    AddOutputFilterByType DEFLATE text/javascript
    AddOutputFilterByType DEFLATE text/plain
</ifmodule>
<ifmodule mod_gzip.c="">
    mod_gzip_on Yes
    mod_gzip_dechunk Yes
    mod_gzip_item_include file .(html?|txt|css|js|php|pl)$
    mod_gzip_item_include handler ^cgi-script$
    mod_gzip_item_include mime ^text/.*
    mod_gzip_item_include mime ^application/x-javascript.*
    mod_gzip_item_exclude mime ^image/.*
    mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifmodule>

# Leverage Browser Caching
<ifmodule mod_expires.c="">
    ExpiresActive On
    ExpiresByType image/jpg "access 1 year"
    ExpiresByType image/jpeg "access 1 year"
    ExpiresByType image/gif "access 1 year"
    ExpiresByType image/png "access 1 year"
    ExpiresByType text/css "access 1 month"
    ExpiresByType text/html "access 1 month"
    ExpiresByType application/pdf "access 1 month"
    ExpiresByType text/x-javascript "access 1 month"
    ExpiresByType application/x-shockwave-flash "access 1 month"
    ExpiresByType image/x-icon "access 1 year"
    ExpiresDefault "access 1 month"
</ifmodule>
<ifmodule mod_headers.c="">
    <filesmatch "\.(ico|flv|jpg|jpeg|png|gif|css|swf)$"="">
        Header set Cache-Control "max-age=2678400, public"
    </filesmatch>
    <filesmatch "\.(html|htm)$"="">
        Header set Cache-Control "max-age=7200, private, must-revalidate"
    </filesmatch>
    <filesmatch "\.(pdf)$"="">
        Header set Cache-Control "max-age=86400, public"
    </filesmatch>
    <filesmatch "\.(js)$"="">
        Header set Cache-Control "max-age=2678400, private"
    </filesmatch>
    <filesmatch ".(js|css|xml|gz|html|woff)$"="">
        Header append Vary: Accept-Encoding
    </filesmatch>
</ifmodule>

web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webserver>
    
    <staticcontent>
      <clientcache cachecontrolmode="UseMaxAge" cachecontrolmaxage="30.00:00:00">
    </staticcontent>
    
    <httpcompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" staticcompressiondisablecpuusage="95" staticcompressionenablecpuusage="60" dynamiccompressiondisablecpuusage="95" dynamiccompressionenablecpuusage="50">
      <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticcompressionlevel="9">
      <dynamictypes>
        <add mimetype="text/*" enabled="true">
        <add mimetype="message/*" enabled="true">
        <add mimetype="application/x-javascript" enabled="true">
        <add mimetype="*/*" enabled="false">
        <add mimetype="application/json" enabled="true">
        <add mimetype="application/json; charset=utf-8" enabled="true">
      </dynamictypes>
      <statictypes>
        <add mimetype="text/*" enabled="true">
        <add mimetype="message/*" enabled="true">
        <add mimetype="application/x-javascript" enabled="true">
        <add mimetype="application/atom+xml" enabled="true">
        <add mimetype="application/xaml+xml" enabled="true">
        <add mimetype="application/json" enabled="true">
        <add mimetype="application/json; charset=utf-8" enabled="true">
        <add mimetype="image/svg+xml" enabled="true">
        <add mimetype="*/*" enabled="false">
      </statictypes>
    </httpcompression>
    
  </system.webserver>
</configuration>

This wasn't enough in our test project though and we were still having issues with "render blocking CSS and javascript". In short, this is referring to code that must be downloaded and executed before the page can be displayed. We overcame this using a combination of Critical Path CSS and lazy loading resources.

Critical Path CSS is not as scary as it sounds — it's actually a pretty straightforward principle: take only the CSS that is essential for displaying the 'above the fold' or viewport content, and then load the rest in the background. There are online generators that make life easier by creating your Critical Path CSS and once generated, it simply needs including in the <head> of your page.

Next we load all the other assets, including the full stylesheet. And in the same process, we actually delete the Critical Path CSS from the <head> as we no longer require it. We used a small (789 bytes) asynchronous loader called LoadJS to do this job for us with the following configuration:

loadjs(
    [
        '/css/main.css',
        '/js/main.js',
    ],
    {
        success: function() {
            
            // Show hidden elements
            var critical = document.querySelectorAll('.critical');
            [].forEach.call(critical, function(element) {
                element.style.display = 'block';
                element.classList.remove('critical');
            });
            
            // Delete Critical CSS
            var critical = document.querySelectorAll('head > style');
            [].forEach.call(critical, function(element) {
                element.parentNode.removeChild(element);
            });
        }
    }
);

Note that we hide all elements below the fold until the relevant styles have been loaded. What this means is that the initial view is just a full screen image and then when the content below is loaded, it is possible to scroll down the page. This happens so quickly on most browsers and connections that it is barely noticeable.

This gets us the rest of the way up to 100% on Google PageSpeed Insights.

The site in question is Bilton's Restaurant and if you look today it only gets 99/100 on both mobile and desktop. This is because we are using TypeKit for the fonts and unfortunately Adobe does not use all the same optimisations that we do on our own server. However, we're still more than happy with that score along with a Pingdom performance grade of 97.

For more information on the project, check out the Bilton's project page.

Want to find out more?

To find out more about how we can help optimising your site or to discuss your project in more detail with us, then get in touch today.