7 Changes I Made to Get My Site to a Perfect Lighthouse Score

Last year, Google announced that “page experience” would start affecting their search ranking. This update is meant to highlight pages that offer “great user experiences”, such as loading quickly and following best practices. They are rolling this out gradually in mid-June, so I’ve been making small improvements to my site over the past month, and I went from an 86 for the Web Vitals to a perfect 100 across all 4 areas that Google’s Lighthouse tool measures: Performance, Accessibility, Best Practices, and SEO. 🎉

Below are the seven specific changes I made recently to get the Lighthouse fireworks animation.

1. Reduce external requests

One of the reasons my site’s performance score was in the “needs improvement” zone was due to three external requests:

Analytics

I had been using Clicky for a very long time without any issues, but more recently, I started seeing others recommend newer services like Plausible and Fathom. I went with Plausible because they had a 30-day free trial vs 7 for Fathom, and their plans are cheaper. I like the simple interface where you can see all the key metrics on the same page, and I like the Google Search Console integration. I’ve now replaced Clicky with Plausible.

Fonts

The last time I redesigned my site, it was when I was working at 18F, around the time the U.S. Web Design System was born. I wanted to borrow some of the design principles, and I chose the Merriweather and Source Sans Pro fonts based on the USWDS typography recommendation.

This time around, I remembered one of my favorite quotes, by Antoine de Saint-Exupéry:

It seems that perfection is achieved not when there is nothing more to add, but when there is nothing left to remove.

I thought about my site’s purpose, which is to educate and help you reach your goals. The main medium I use for this is writing, and so what matters most is that my writing is clear and legible. A font is not going to change the clarity, but it can certainly affect readability.

While I could have tried hosting the Google Fonts locally to improve performance, I thought why not make it as fast as possible by using the System Font Stack? Today’s modern fonts that come pre-installed on most computers are beautiful, like Apple’s San Francisco font, which is what you’re seeing right now if you’re reading this on a Mac running El Capitan or later.

Host Font Awesome Locally

With one remaining external request, I looked into how to host the Font Awesome fonts myself, since I was only using a handful of them. This was a bit tricky to figure out, but here’s what I ended up doing:

.#{$fa-css-prefix}-flickr:before { content: fa-content($fa-var-flickr); }

.#{$fa-css-prefix}-github:before { content: fa-content($fa-var-github); }

.#{$fa-css-prefix}-rss:before { content: fa-content($fa-var-rss); }

.#{$fa-css-prefix}-soundcloud:before { content: fa-content($fa-var-soundcloud); }

.#{$fa-css-prefix}-twitter:before { content: fa-content($fa-var-twitter); }
$fa-var-flickr: \f16e;

$fa-var-github: \f09b;

$fa-var-rss: \f09e;

$fa-var-soundcloud: \f1be;

$fa-var-twitter: \f099;
@import 'variables';
@import 'mixins';
@import 'core';
@import 'icons';
@import 'screen-reader';
@import "font-awesome/fontawesome.scss";
@import "font-awesome/brands.scss";
@import "font-awesome/solid.scss";

2. Optimize images

While testing some of my articles that have screenshots, the score went down because the images were too big, or were in formats like PNG that don’t compress well. The Lighthouse tool recommended using the WebP format, so I tried converting a PNG to WebP, and I was impressed with the results. The file size was reduced by more than half, without any noticeable loss in quality.

Then I made the mistake of deploying this change without fully testing it. It turns out the WebP format is not yet supported by all browsers. For example, Safari only supports it in Big Sur or later. I have an M1 MacBook Air with Big Sur that I use most of the time, but I also have an Intel iMac that is still running on Catalina, which is how I discovered this issue. The funny thing is that I did check the Can I Use website for compatibility, but I didn’t read it closely enough.

So then I converted it to a JPEG, which ended up being even smaller than the WebP (18KB vs 70KB), but with a little loss of quality, although it didn’t really matter for that particular article. This brought the score back up.

I still have to go through the rest of my articles to optimize more images, and I also need to specify the size of each image, as recommended by Lighthouse.

3. Concatenate JS

I use Middleman to generate this site, and it supports Sass out of the box, which is what allows all my CSS files to get combined into a single all.css file. This can help performance because the browser is only making one request instead of multiple requests for multiple CSS files.

I think that concatenating JavaScript used to be supported in Middleman via the Rails Asset Pipeline, but I don’t think I’ve ever used that feature, so I don’t know for sure. In the latest version of Middleman, they recommend using an external pipeline, so I started researching the best tool for the job. Then I remembered what Antoine would say, and I realized that I only use 2 JS tools:

Each one needs to be called with a tiny bit of Javascript, and for some reason, I had put each in a separate file instead of inline. Perhaps from old habits of keeping code organized. So then I created a source/javascripts/all.js file and copied and pasted the minified JS for both AnchorJS and highlight.js, and then added the small initialization code for each inline, for example:

%script
  hljs.highlightAll();

Now I only had a single JS file for the browser to fetch.

4. Conditional JS

In addition to combining the 4 different JS files into a single one, I also noticed that only certain pages on my site need anchors and syntax highlighting. So, I added a conditional to only load the all.js file on posts that need them. I identified those posts by adding js: true to the YAML front matter. The front matter is the part at the top of the Markdown file where you specify various attributes for the post. This is supported by most popular static site generators.

Then, in my layout.haml file, I passed in the value of the js variable to my _scripts.haml partial:

= partial 'header'
%body
  = partial 'navigation'
  %section#main
    = yield
  = partial 'footer'
  = partial 'scripts', locals: { js: current_page.data.js }

And in my _scripts.haml:

- if js
  = javascript_include_tag 'all'

5. Minify CSS

This was the easiest change, and I don’t remember why I had it disabled, but it was as simple as activating it in config.rb:

configure :build do
  activate :minify_css
end

Minifying CSS makes the file smaller by removing characters that the browser doesn’t need to interpret the code, such as whitespace.

6. Preload CSS and fonts

Lighthouse recommended I preload critical assets, such as my CSS file and the Font Awesome fonts.

For the fonts, I added these lines to my _header.haml:

%link{href: "/webfonts/fa-brands-400.woff2", as: "font", type: "font/woff2", rel: "preload", crossorigin:"anonymous"}

%link{href: "/webfonts/fa-solid-900.woff2", as: "font", type: "font/woff2", rel: "preload", crossorigin:"anonymous"}

For the CSS, I just needed to add rel: "preload" and as: "style", but also specify the regular stylesheet:

= stylesheet_link_tag "all", rel: "preload", as: "style"
= stylesheet_link_tag "all"

From the Google documentation, it wasn’t clear that both were needed. If I remove the original stylesheet and leave only the preloaded one, I get an error in the browser console that the CSS file was preloaded but never used.

7. Add aria label and title to RSS feed

At this point, I was well in the green (90+ score), but not yet at 100. I lost a point or two due to accessibility issues. When I originally added the RSS icon a while back, I forgot to add a title, and Lighthouse also recommended an aria-label, so this is how I fixed it:

= link_to '/feed.xml', { "aria-label": "RSS Feed" } do
  %i.fas.fa-rss{title: "RSS Feed"}

This was enough to get me a perfect 100 score for Performance, Accessibility, Best Practices, and SEO. 🎉

Lessons learned

One mistake I made was to move my CSS file from the <head> section down to the bottom. This caused the Cumulative Layout Shift to increase because the site initially loaded without CSS, then looked different once the CSS file was loaded. Every visual change on the site while it loads increases the CLS.

Once I moved the CSS back to the top, this reduced the CLS, but PageSpeed Insights still kept telling me my average CLS was too high, which made my site fail the Core Web Vitals test. It was surprising to me that I could have a perfect Lighthouse score, but also fail the Core Web Vitals.

Then I realized that it was probably because my JS file was loading after the main content, and because many of my posts have code examples, those appear unstyled at first, and then the text shows up once the JS is loaded, which contributes to the CLS.

So then I moved the JS back to the header and preloaded it as well, since it’s a critical asset:

- if current_page.data.js
  = javascript_include_tag "all", rel: "preload", as: "script"
  = javascript_include_tag "all"

That fixed the CLS issue, which allowed my site to pass the Core Web Vitals, but now it was complaining that my Total Blocking Time was too long because JS blocks rendering until it’s done. Argh!

So then I thought I’d try separating highlight.js from AnchorJS since the anchor links are not as critical, and they don’t affect the CLS since they only show up when you hover. So I went back to the separate anchor.js file that I had and put it before the closing </body> tag, while highlight.js was in the <head> tag and preloaded.

This didn’t make enough of a difference.

The easiest thing I could try next was to replace highlight.js with another similar tool. I had been meaning to try Prism, so this was the perfect time. The minified file ended up being almost half the size of highlight.js, so this was already promising. I tested it locally, and everything looked good. I just had to adjust the CSS a bit, namely the font-family, font-size, and line-height.

I deployed this change, and tested a few pages with PageSpeed Insight, and finally, I was in the green again!

Next steps

There is still one more thing I can remove to achieve perfection: syntax highlighting using JS. Looking through my Git history, I see that I added syntax highlighting back in 2016 with middleman-syntax, which uses Rouge, just like Jekyll does.

And then about 2 months later, I replaced Rouge with highlight.js, and the only comment I left myself was “hljs is better maintained and has more themes.” I don’t remember if there were other issues I ran into with middleman-syntax and/or Rouge.

Now that I have more experience, I know that it shouldn’t be necessary for the browser to download and run JavaScript to highlight code. So I’m going to take another look at middleman-syntax, or maybe switch to Jekyll.

I hope this was useful to you, and if you have experience with web performance, I’d love to hear your tips.