Speed is an important factor for user experience for any site or mobile application: multipleorganizations have been studying at the effects of poor performance and at ways to improve it. More recently, Google announced the upcoming changes in terms of Page Experience ratings for websites. Page Experience will be used as an input to the ranking signals that determine whether your website or page is shown in search results.
Let's start from the outcome! Here is what it looks like right now using web.dev Measure after the updates I've made - and keeping in mind that these result may fluctuate over multiple audits.
There is still work to do to improve, but I have discovered a few easy tricks to speed up most of my pages. In terms of findings, I would say that the main areas of improvement that were found straight away are:
Image sizing / compression / deferred loading.
3rd party scripts.
A few accessibility tweaks, for example: remember to add alternative image descriptions to the images when authoring a blog post. This is supported in Ghost but optional so you have to remind to do it.
I have also discovered that I need to be very wary of these fancy YouTube video and Twitter card embeds that seem to absolutely be killing the performance of the pages as seen in the last example.
Finally - it was very pleasing to see that the solid foundations put in place in the past were already putting me ahead of the curve. If you are interested, have a look at some of my older articles:
Let's now look at the things I did in this round of tweaks.
One of the very first obvious things that wereflagged was the fact that the images, especially on the homepage were not compressed optimally or served using one of the best formats available.
Turns out the easiest way to achieve this is by using a CDN that automatically optimize the format of the picture. This is done depending on the capabilities of the browser connected. When possible, it is best to take advantage of modern compression image formats such as WebP, Jpeg2000 or JPEG XR.
If you use Cloudflare, you can use the Polish service which will automatically take care of this for you. This is part of the paid Pro offering which comes with other goodies such as Enhanced WAF (Web Application Firewall), caching analytics and other mobile optimizations. Polish is a a turn-key feature which will seamlessly inject the image optimizations to all traffic served on your configured DNS Zone and it is very easy to set up.
Otherwise, you can look at an alternative available for free, especially if you have a small personal blog. Ghost can integrate with Cloudinary. Cloudinary is a media optimisation platform and they have a free plan which can work for you, depending on the amount of traffic you get.
In my case, I wanted to try the Cloudinary option without using the storage. In other words, I want the images to be uploaded and stored on my server, but I then want to use the Cloudinary fetch API to automatically pull an optimised version. To do this, I changed the code of my version of Casper (learn more about that in this article) and followed the Ghost tutorial. Here's a sample of my change from post-cards.hbs
Summarizing: the image will be loaded from Cloudinary and certain transformations will be applied using the Cloudinary image fetch API:
w_XXX: this will resize the width of the to the value specified XXX
c_fit: ensure the image stays within the bounding box provided. In our case we only provided a width so this should not be required
q_auto , f_auto , dpr_auto: let Cloudinary decide the best quality, format and device pixel ratio depending on the connecting device.
You can see the below files for more details of the changes done:
Please note: this change will not cover images that are part of the blog post content, as this is not exposed in the theme.
A different approach should be taken here. For example, we could rewrite the HTML on the fly with a Cloudflare Worker. We will look at this again in the next step where we introduce it.
The inspiration for this tweak came from this blog post. In short, while the Ghost team is busy adding native support for this in the platform itself, we can implement lazy loading ourselves via a Cloudflare worker.
This worker will intercept all requests on the blog domain, and look for image tag elements. If the img tags found do not have a loading="lazy" attribute, it will add it to them, then serve the updated HTML document to the browser.
This ensures that images which are not yet visible to the user are not loaded immediately, greatly speeding up the page! You can find out more about the feature that is now available in most major browsers.
There is one aspect, that the worker code included by the aforementioned blog author is very wide-ranging: the worker operates on every request hitting the blog, and then inspects and modifies anyimg tag found.
This has caused a problem to me while I was authoring this blog post - specifically, it messed up the Ghost Admin API as it tried to return the post contents in JSON for the above post-cards.hbs code snippet. The worker stepped in, including the tag and breaking the JSON escaping, ultimately causing an HTTP 500 error in the editor.
I have disabled the worker, then modified the original code to look like this
There is quite a lot going on here so let's dissect this further:
The handleRequest function has been changed, it now looks at the request url and determines whether we should run the main worker logic or bypass it depending on the context path value. EXCLUDED_PATHS is an array containing specific Ghost paths we want to bypass: /ghost , /rss , /content, /assets. We bypass them either because we don't want the substitution to happen at all, or because we already know it will not be needed for these requests. For example, a request for a static asset does not need any HTML rewriting.
Similarly, the ElementHandler now looks at the element and inspects both the src and class attributes. We only want the worker to operate if the image class is one of the list. Please note that I added feature-image myself in the partial I created earlier feature-image.hbs - so that I can target post feature images. Update 19 July 2020: I have reverted the feature-image customization as it was causing CLS (Cumulative Layout Shift) - as reported by the Google Webmaster Search Tools.
If the image is one of the ones we want to operate on, then we add the loading=lazy attribute if not already defined (same as in Stanislas's original code).
I added another change so that we can load our post images from Cloudinary as well. Remember that these were out of reach from our earlier change? With the Cloudflare worker, I can check if the image URL starts with a specific value, and if so I can prepend the Cloudinary fetch URL to it. Pretty neat! However this won't work if the image has a srcset defined, as I am not rewriting that for the time being. Maybe as a future enhancement.
If you are wondering what ENV_LOCAL_CONTENT_URL and ENV_CDN_FETCH_URL are, these are Cloudflare Worker Environment Variables. In other words, the code will be able to resolve them when running. This allows the worker code to be more configurable: you can set the variables either in the Cloudflare UI or (recommended) using wrangler.
🔥 Update 28 August 2020: I have created a separate worker script which works with Ghost without further configuration and is specifically meant to be used with Ghost and Cloudflare's own Image Resizing capability. You can find the worker here.
You can also auto deploy it by using the below button. Refer to the earlier link for setting up the worker route to activate it.
External connection hints
One of the suggestions from Lighthouse was around the fact that the site was not using any preloading / external connection hints for the browser. For example, the Casper theme uses jQuery and it loads it from code.jquery.com.
Turns out there is a way we can tell the browser that the document will need to load resources from a certain URL. In my case, all I needed to do was to add the following tags in the Code Injection tab in Ghost Admin:
In this way, the browser can warm up the handshake with these external domains: as you can imagine, the browser will need to set up an encrypted TLS connection to get the data it needs. The tag above gives it the chance to start doing some of that work before it knows what it needs to download exactly.
I recommend reading the following blog article if you want to understand more about preloading / prefetching and other types of browser hints.
Turns out someone else had already thought about that and solved it: you can see the solution in this blog post, which is related to another blogging platform but works perfectly well with Ghost too!
I still need to figure out how to shave off further delays for pages that include third party integrations and widgets. It's almost comical that Page Speed Insights is telling me that a Youtube embed is killing the performance of my beloved blog article 🤣
I hope that the above tips and tricks will help you in setting up a super-fast page, regardless of whether you are using Ghost or another system. Some tips are trivial to roll out and will make a difference.
Let me know in the comments below if you have any other insights or tricks to share with me, and thanks for reading!