Skip to Main Content
Emma svalstad 1273069 unsplash

Page Speed Optimisation - Latest Techniques

Introduction

Almost 2 years on from writing my last article about Page Speed Optimisation there have been a few changes which need to be addressed. Before I start I want to make it clear that in my opinion Page Speed isn’t something that is done once and never revisited. Over time new ways of making sites more performant are developed and the tools we use start to measure different metrics.

Our perception of speed also changes over time, remember those old dial up sites? We used to think some sites were pretty fast, but compared with today's standards they would be actually seem pretty slow. It is important to keep up with the latest tricks otherwise a site you optimised 2 years ago to score 100/100 can easily fall down to 40/100. So let’s look at how we can improve on our previous page speed optimisations.

Rules for our htaccess file

Initially we always thought of the htaccess file as a kind of dark arts here at A Digital, these days though we’ve really gotten to grips with them. If you don’t use htaccess files then please feel free to use these rules as a guide to generate your own rules. Also we have used Craft in our example project but I’ve left out the default rules so that the rules can easily be added into any cms or custom solution which uses htaccess files. Just make sure you place them at the bottom of the file outside of any existing rulesets or ifModule blocks.

Our first block we shall be adding is to allow our htaccess file to recognise various mime types. This will help us when defining rules later on around various fonts and svgs.

<IfModule mod_deflate.c>
    AddType image/svg+xml svg svgz
    <ifmodule mod_mime.c>
        Addtype font/opentype .otf
        Addtype font/eot .eot
        Addtype font/truetype .ttf
        AddType application/x-font-woff .woff
        AddType application/x-font-woff2 .woff2
        AddEncoding gzip svgz
    </ifmodule>
    AddOutputFilterByType DEFLATE text/html text/plain text/css application/json
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE text/xml application/xml text/x-component
    AddOutputFilterByType DEFLATE application/xhtml+xml application/rss+xml application/atom+xml
    AddOutputFilterByType DEFLATE image/x-icon image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype font/truetype font/eot
</IfModule>

Now that we’ve added our mime types we can specify some specific header to control our Vary: Accept-Encoding and Expires Caching, something which pagespeed tools often highlight when not set up correctly. The max-age values can of course be adjusted to longer or shorter periods depending on your needs, but we find that these values work best for us currently. This is also the block where you can set additional Access-Control-Allow-Origin rules for any incoming connections you may wish to allow. This can be done on a per file type basis, like we have done for our fonts.

<IfModule mod_headers.c>
    <filesMatch "\\.(ico|pdf|flv|jpg|jpeg|png|gif|swf|ttf|otf|woff|woff2|eot|svg)$">
        Header set Cache-Control "max-age=31557600, public"
    </filesMatch>
    <filesMatch "\\.(css)$">
        Header set Cache-Control "max-age=604800, public, must-revalidate"
    </filesMatch>
    <filesMatch "\\.(js)$">
        Header set Cache-Control "max-age=216000, private"
    </filesMatch>
    <filesMatch "\\.(xml|txt)$">
        Header set Cache-Control "max-age=216000, public, must-revalidate"
    </filesMatch>
    <filesMatch "\\.(html|htm|php)$">
        Header set Cache-Control "max-age=1, private, must-revalidate"
    </filesMatch>
    <filesMatch "\.(js|css|xml|gz)$">
        Header append Vary: Accept-Encoding
    </filesMatch>
    <filesMatch ".(eot|ttf|otf|woff|woff2)">
        Header set Access-Control-Allow-Origin "*"
        Header append Vary: Accept-Encoding
    </filesMatch>
</IfModule>

Finally we need to add some rules for gzip output. The list below covers most of the types you will need. You’ll also notice some duplication from the rules above in our first block to highlight how you could reduce this section significantly if needed, but we feel that having a separate block with a rule per line is a much clearer way of displaying all of our types so that you can easily see if we need to add any additional rules or not.

<ifModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/text
    AddOutputFilterByType DEFLATE text/html
    AddOutputFilterByType DEFLATE text/plain
    AddOutputFilterByType DEFLATE text/xml
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE application/x-javascript
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE text/javascript
    AddOutputFilterByType DEFLATE image/svg+xml
    AddOutputFilterByType DEFLATE font/opentype
    AddOutputFilterByType DEFLATE font/otf
    AddOutputFilterByType DEFLATE font/eot
    AddOutputFilterByType DEFLATE font/ttf
    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-font-woff
    AddOutputFilterByType DEFLATE application/x-font-woff2
</IfModule>

Once these htaccess rules have been implemented you should see your pagespeed scores improve. Of course as discussed in The Impossible PageSpeed Score you can never get to 100% because of google's own analytics code having suboptimal caching expirations, but this is unavoidable. Essentially any time that you load in resources from other domains, you won’t have control over their cache expirations, which leads us nicely into our next section.

Optimising Third Party Fonts

Most javascript libraries such as jquery can be saved locally and retain their functionality, this will allow you to minify the code, set its cache expiration headers, and prepend it into your js file to reduce the number of scripts loaded. When dealing with fonts though, not all of them can be saved locally. To be clear, our main concern around fonts isn’t necessarily the cache expirations, but the fact that they sit in the critical path and are render blocking resources. This means that the browser will wait until they have loaded before rendering the page, which slows the page down. To get around this we need to use some clever new techniques.

For the fonts that can be saved locally, great, do this and define your font-face using the src:url attributes. For libraries such as cloud.typography.com, as well as many others, we need a different approach. First of all, check your font libraries example code to see if it can be loaded in an asynchronous way. If this is an option then this should be implemented, if not then we can create our own method to do this.

Essentially we need to add into our main javascript file to append our font script to the document head. This will mean that the initial page load doesn’t have a reference to the font library, and once javascript has loaded it is added. The code that cloud.typography.com provides us with is as follows:

(function() {
    var typographyCss = document.createElement('link');
    typographyCss.rel='stylesheet';
    typographyCss.type='text/css';
    typographyCss.href='https://cloud.typography.com/<code_here>/css/fonts.css';
    (document.getElementsByTagName('head')[0]||document.getElementsByTagName('body')[0]).appendChild( typographyCss );
})();

Our next step is to use the css font-display property with a value of swap on all of our @font-face rules across the site, support for older browsers is detailed in Font Display for the Masses. This will allow our browsers to load a default font-face that we’ve set before calling our loaded font libraries. This removes the render blocking aspect and allows the text to remain visible during webfont loads.

Imager Plugin Configuration

This is a Craft specific section but the optimisations will be needed on most sites regardless of which platform they run on. First of all, we need to ensure that we are using the Imager plugin, other options of course are available but this is our go to solution for what we want to achieve. The base install of imager has some nifty tricks but to truly unlock its potential we are going to install 2 additional libraries to our server.

sudo apt-get install jpegoptim​
sudo apt-get install optipng

Running both of the above commands will install the libraries that we want to utilise. To access these we need to go to our config folder and create a file named imager.php. Within this config file we will add the following:

'imagerSystemPath' => '@mediaPath/imager/',
'imagerUrl' => '@mediaUrl/imager/',
'jpegQuality' => 60,
'pngCompressionLevel' => 5,
'webpQuality' => 60,
'optimizers' => ['jpegoptim', 'optipng'],
'optimizerConfig' => [
    'jpegoptim' => [
        'extensions' => ['jpg'],
        'path' => '/usr/bin/jpegoptim',
        'optionString' => '-s',
    ],
    'optipng' => [
        'extensions' => ['png'],
        'path' => '/usr/bin/optipng',
        'optionString' => '-o2',
    ]
]

Now that we’ve configured our imager plugin we should get improved image optimisation compared to a base install of the plugin. We’ve also used some aliases which we’ve defined in our general.php config file. Further optimisations can be also performed if you use imgix but we wanted to show a setup that can work without needing any additional apis or subscriptions.

Pagespeed example 98 percent

Long pages with lots of product loops and images can still be performant when optimised correctly!

Template Tags and Webp

It’s not enough to just configure our Imager plugin with a config file and expect it to produce performant images, we also need to make sure that we are using our template tags efficiently. Imager provides us with a large amount of options so we are going to go through what our default tag structure looks like.

Rather than build up from a simple integration adding layers of complexity at each point, I’m going to just show the finished product and explain what each part does and why we’ve chosen to do it this way. Firstly, we want to set a new variable in our layout file which will be available to all of our templates, we are going to be using this within our caching tags appended to the key.

{% set webpImages = "" %}
{% if craft.imager.serverSupportsWebp and craft.imager.clientSupportsWebp %}
    {% set webpImages = "-webp" %}
{% endif %}

The reason we set the above variable is because not all browsers support WebP, so if we were to cache the output on a browser which does and reload it into a browser which doesn’t, we will have lots of broken images. This wouldn’t be good practice for a modern site regardless of any speed optimisations we’d made.

WebP is a next gen image format which google has been pushing for awhile now in its pagespeed scores. Other formats are available such as JPEG 2000 and JPEG XR. The next gen image formats often provide better compression than traditional formats. We are using WebP with our imager configuration in our examples.

Our next step is to configure our cache tags using our new variable which we do like so:

{% cache globally using key craft.app.request.pathInfo ~ webpImages if craft.app.config.general.cache %}
    {# insert code blocks here #}
{% endcache %}

Caching will also help our pagespeed optimisation in Craft as it significantly reduces our loading times once a page has been cached. Caching can be set to true or false on a per environment basis in our general config file. Now that we have our caching tags in place and access to a variable which detects browser compatibility for webp we can move on to our image tags.

{% if webpImages %}
    {% set fallbackImage = craft.imager.transformImage(alias('@mediaUrl')~"/general/placeholder.png", { height: 400, ratio: 3/2, interlace: true, mode: 'letterbox', letterbox: { color: '#ffffff', opacity: 0 }, pngCompressionLevel: 0, format: 'webp' }) %}
{% else %}
    {% set fallbackImage = craft.imager.transformImage(alias('@mediaUrl')~"/general/placeholder.png", { height: 400, ratio: 3/2, interlace: true, mode: 'letterbox', letterbox: { color: '#ffffff', opacity: 0 }, pngCompressionLevel: 0 }) %}
{% endif %}
{% set asset = entry.mainImage.one() %}
{% set transformedImages = null %}
{% set transformedImagesWebP = null %}
{% if asset is not null %}
    {% set transformedImages = craft.imager.transformImage(asset, [
        { width: 1200 },
        { width: 940 },
        { width: 470 },
        { width: 290 }
    ], { ratio: 6/4, interlace: true, mode: 'crop', position: asset.getFocalPoint() }) %}
    {% if webpImages %}
        {% set transformedImagesWebP = craft.imager.transformImage(asset, [
            { width: 1200 },
            { width: 940 },
            { width: 470 },
            { width: 290 }
        ], { ratio: 6/4, interlace: true, mode: 'crop', position: asset.getFocalPoint(), format: 'webp' }) %}
    {% endif %}
{% endif %}
<picture>
    {% if webpImages %}
        <source data-sizes="100vw 50vw" data-srcset="{{ craft.imager.srcset(transformedImagesWebP)|default(fallbackImage.url) }}" type="image/webp">
    {% endif %}
    <img src="{{ craft.imager.base64Pixel(2,1) }}"
        data-src="{{ craft.imager.placeholder({ width: 940, ratio: 6/4, interlace: true, mode: 'letterbox', letterbox: { color: '#ffffff', opacity: 0 }, pngCompressionLevel: 0 }) }}"
        data-srcset="{{ craft.imager.srcset(transformedImages)|default(fallbackImage.url) }}"
        data-sizes="100vw 50vw"
        alt="{{ asset.title|default(entry.title) }}"
        class="db lazyload">
</picture>

You might think that’s a lot of code to generate a single image, but there are a few things going on here which we shall explain. The initial part of our code sets up a fallback image which can be used whenever an image doesn’t exist, this is called in the |default() twig filter. Next we load our asset into a variable and set some additional variables to false for later usage in our conditionals.

At this point we call our imager plugin to generate our transformed images, you will notice that we are setting multiple widths as we intend to use srcset on our img tags, our position is also taken from the assets focal point which can be set within Craft on any uploaded image. It is also important to note that the interlace option has been set to true. This allows browsers to load a pixelated version first, and progressively layer up the quality rather than trying to get the best quality straight away. This helps in making content visible quicker instead of leaving an empty space whilst the image is being fetched.

Now that we’ve generated our transformations with imager, we can use them in our html to display them on the page. In our example we’ve used an img element with srcset and sizes, but we’ve also wrapped this with a picture element, and above it sits a source element for our WebP image. This allows us to load the WebP image if the browser supports it, or default to a standard image if it doesn’t.

You’ll also notice that we’ve used data-src, data-srcset and some other attributes instead of the standard properties, this is because we are lazyloading all of our images. To make this work, you will need to download the lazysizes javascript library and call it like so:

<script src="lazysizes.min.js" async></script>

Then you need to add a class of lazyload to your elements before setting the correct data attributes. By doing this we ensure that any images outside of the viewport aren’t loaded. This will speed up our initial page load as there is less data coming down through the connection. We also use imager one last time to generate a small base 64 pixel to populate the image tags src attribute. We’ve also taken this one step further by combining the libraries script into our main compiled js file.

Conclusion

In this article we’ve gone through a fairly complex setup for our images, optimised our font loading, and added some caching rules into both our htaccess file and our templates. By making these changes we can achieve a higher score in google pagespeed insights but we should have also seen a visible increase in the speed of the site ourselves.

Page Speed Optimisation is a job which is never truly completed and you will always need to keep up with the latest trends. By combining a few methods we’ve previously covered (Page Speed Optimisation, Lazy Loading and High Availability Hosting) with the techniques we have gone through today, you can create a truly performant site without sacrificing any functionality.