How I Scored a 100 in Lighthouse Performance for My Jekyll Website

After the Google Speed Update, I thought it would be an interesting exercise to measure and optimise the performance of my Jekyll website hosted on GitHub Pages.

This post details the steps I took, and my thought process.

Measure

To measure the baseline performance, I used the Lighthouse tool available via Chrome Developer Tools. Lighthouse gives an in-depth performance analysis and highlights opportunities to improve performance.

Lighthouse results: Before

Optimise

A score of 98 was a good starting point, but Lighthouse had identified a number of opportunities for optimisation.

I looked at ways to implement its recommendations and speed up page load.

Properly size images

Serve images that are appropriately-sized to save cellular data and improve load time.

The main strategy for serving appropriately-sized images is called “responsive images”. With responsive images, you generate multiple versions of each image, and then specify which version to use in your HTML or CSS using media queries, viewport dimensions, and so on.

Properly size images

A number of Jekyll plugins add responsive images, but Jekyll Picture Tag looked the most complete. It automatically resizes images and provides a tag that can be inserted into templates or content to generate the required HTML for displaying responsive images.

Note the following:

Serve images in next-gen formats

Image formats like JPEG 2000, JPEG XR, and WebP often provide better compression than PNG or JPEG, which means faster downloads and less data consumption.

Jekyll Picture Tag can also generate WebP images by configuring its formats option, making this an easy win.

Eliminate render-blocking resources

Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles.

All JavaScript was already either inline or deferred using the async attribute, leaving just the CSS to be optimised.

The critical rendering path represents the steps that the browser takes to render a page. We want to find the minimum set of blocking CSS, or the critical CSS, that we need to make the page appear to the user.

Understanding Critical CSS

I ran Critical against my site to extract the critical CSS. This was added inline in the site head using a Jekyll include tag. Non-critical styles were deferred using link rel=“preload”.

I also added a noscript element to load the stylesheet when JavaScript isn’t enabled, and included the loadCSS polyfill script for browsers that don’t support link rel=“preload”.

<head>
<style>{% include critical.css %}</style>
<link rel="preload" href="/assets/stylesheets/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/stylesheets/main.css"></noscript>
<script>/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>
</head>

Defer unused CSS

Remove unused rules from stylesheets to reduce unnecessary bytes consumed by network activity.

I used UnCSS to remove unused CSS rules.

Note: Since UnCSS only runs on HTML files, it must run after Jekyll has generated the site. The “UnCSSed” stylesheet can then be added to the built site.

All text remains visible during webfont loads

Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading.

Zach Leatherman’s A Comprehensive Guide to Font Loading Strategies covers various options for using the font-display CSS feature.

I chose to replace webfonts with system fonts, rendering (pun intended) this a non-issue. No webfonts = no loading.

The number one reason for using system fonts at all is performance. Fonts are typically one of the largest/heaviest resources loaded on a website. If we can use a font already available on the user’s machine, we can completely eliminate the need to fetch this resource, making load times noticeably faster.

The New System Font Stack?

Serve static assets with an efficient cache policy

A long cache lifetime can speed up repeat visits to your page.

When possible, cache immutable static assets for a long time, such as a year or longer. Configure your build tool to embed a hash in your static asset filenames so that each one is unique.

Uses inefficient cache policy on static assets

The Google Developers Lighthouse reference recommends a cache lifetime of a year, provided you have a way to invalidate and update cached responses.

GitHub Pages serves all assets with a cache lifetime of 10 minutes. This isn’t configurable, but websites set up behind Cloudflare have a workaround. Cloudflare allows the cache lifetime to be overridden by setting the Browser Cache Expiration to a higher value. I set mine to 1 year.

To allow me to force visitors to download a new version of cached assets (eg. for stylesheet updates), I implemented cache-busting by adding a timestamp to the query string of asset URLs.

Minimize Critical Requests Depth

The Critical Request Chains … show you what resources are loaded with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load.

No further changes were required as the critical request chain showed the high priority resources were the stylesheet and webfonts. Previous changes had already deferred the stylesheet and removed webfonts.

Results

Running Lighthouse again after making the changes resulted in a perfect score of 100.

Lighthouse results: After

Conclusion

While the exercise was successful in achieving a perfect score in Lighthouse, it highlighted some of the limitations of the Jekyll + GitHub Pages combination.

  • Many Jekyll plugins that are useful for managing site assets (images, CSS, JavaScript) are not supported by GitHub Pages, removing the ability to use its built-in support for building Jekyll sites. Sites using these plugins must be built locally or on a build server.
  • CSS and JavaScript optimisation tasks, such as extracting critical CSS, are better supported by tools from the JavaScript ecosystem. These naturally integrate better with JavaScript build tools such as webpack, Grunt, and Gulp. Integrating with Jekyll means having jekyll build as a step within a JavaScript build pipeline, rather than having Jekyll build everything.

As shown, it is possible to build and host a performance optimised site with Jekyll and GitHub Pages.

To take full advantage of Jekyll’s available plugins, however, you need to build your site outside of GitHub Pages. Using a JavaScript build pipeline gives you access to even more tools for optimisation.

Further reading