profile

Hi! I'm Moncef Belyamani

7 Steps to a Faster Website

Published almost 3 years ago • 10 min read

Hi Reader, I hope you're doing well.

My latest post is all about web performance, and how I used Google's Lighthouse and PageSpeed Insights to improve my site. I go over 7 specific changes I made, and most of them are applicable to any website. I hope you learn something new today that you can use to make your own site better.

First, I wanted to share a few links and tips.

🕵🏽‍♀️ DuckDuckGo has a Cheat Sheet option for certain topics. To see what I mean, go to DuckDuckGo, then search for "rails cheatsheet", or "ruby cheatsheet", or "jekyll cheatsheet".

🗣 In April, I submitted a few talk proposals to the Euruko Ruby conference, and one of my Lightning Talks got accepted! It was based on my popular post, The 6 Characters That Could Bring Down Your Rails App, that was featured in the Ruby Weekly newsletter earlier this year. The conference took place online about 2 weeks ago, and the videos are up now. Here is my Euruko Lightning Talk.

👨‍🍳 I recently made 2 dishes that were a hit with my family: Thai Basil Sesame Cashew Chicken with Coconut Rice, and Honey-Apple Bread Pudding.

🎥 Speaking of lightning talks, my friend Marcin Wichary, a very talented and creative person, did an amazing talk called Four Laps. It was an internal talk at Figma, where he works as a Design Manager. Marcin and I were part of the 2013 fellowship at Code for America.

👩🏻‍🎓 Today is my oldest daughter's graduation from Kindergarten!

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:

  • I downloaded the "Free for Web" version from their download page (version 5, not the 6.0 beta version).
  • I created a source/stylesheets/font-awesome folder and put in it only these files from the downloaded scss folder:
    • _core.scss
    • _icons.scss
    • _mixins.scss
    • _screen-reader.scss
    • _variables.scss
    • brands.css
    • fontawesome.css
    • solid.css
  • I opened up the _icons.scss file and removed everything except for the 5 icons that I use:
.#{$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); }
  • I did the same thing with the _variables.scss file and only kept the 5 I use below line 19:
$fa-var-flickr: \f16e;

$fa-var-github: \f09b;

$fa-var-rss: \f09e;

$fa-var-soundcloud: \f1be;

$fa-var-twitter: \f099;
  • I modified fontawesome.css to only import the modules I needed:
@import 'variables';
@import 'mixins';
@import 'core';
@import 'icons';
@import 'screen-reader';
  • I created a source/webfonts directory and copied over all the fonts from the downloaded webfonts folder except for all the fa-regular fonts because I don't use them.

  • In my existing source/stylesheets/all.css.scss, I added the Font Awesome imports:

@import "font-awesome/fontawesome.scss";
@import "font-awesome/brands.scss";
@import "font-awesome/solid.scss";
  • I removed the external call to https://kit.fontawesome.com/81391da.js

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:

  • AnchorJS to automatically generate anchor links for headings (hover over "Concatenate JS" above and you will see a link icon appear).
  • highlight.js for syntax highlighting of the code examples.

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.

As always, if there's any question you have, or feedback on my guides, just hit reply. I read everything you send.

Did you find this email useful? Forward it to someone who will enjoy it too!

Have a great weekend,

Moncef

Hi! I'm Moncef Belyamani

Every week, I send out an automation tutorial that will save you time and make you more productive. I also write about being a solopreneur, and building helpful things with Ruby. Join 2853 others who value their time.

Read more from Hi! I'm Moncef Belyamani

Hi Reader 👋🏼 Happy Sunday! I hope you and your loved ones are doing well. Earlier this week, I found a great use case for the 1Password CLI that hadn't occurred to me before. I'm gonna use it a lot more often whenever I can! If this email doesn't look right, or if you prefer reading on my site, you can click the title link below. Automate GitHub API Calls With Ruby, Keyboard Maestro, and 1Password CLI One of the perks of the “Ultimate” version of Ruby on Mac is access to the private GitHub...

over 1 year ago • 5 min read

Hi Reader! This week's automation guide is about a free but powerful Mac app called Bunch. I had heard of it years ago but never took the time to explore it in detail. Until now, and it has proven very useful so far. I'm not sure how the code samples will look like in your email, so you might prefer to read this on my site by clicking the title below. Automate Context Switching With Bunch You sit down to work on a feature, and wake up your Mac. Oh hey, Slack is open. You decide to check it...

over 1 year ago • 12 min read
PopClip default extensions

Hi Reader! This week's automation guide is about a little-known app called PopClip. PopClip was originally released in 2011, but I didn’t hear about it until four years ago, and I’m sure there are still a lot of people who don’t know about it. It’s one of the many useful apps you can discover and quickly install with the “Ultimate” version of Ruby on Mac. You can pick and choose from hundreds of Mac apps, fonts, and dev tools in the included Brewfile, and Ruby on Mac will install them all at...

over 1 year ago • 2 min read
Share this post