Perf’s Up
April 12, 2020
Last month, Zach Leatherman retooled the list of sites built with his static-site generator Eleventy, transforming what was originally a flat list into a leaderboard of sorts:
Sites with Lighthouse scores greater than or equal to 90 are ordered by performance. The remainder are ordered randomly. Performance rankings are updated approximately once per week on Sunday.
I have a single-serving site on the list, Eats & Drinks. It's nothing special, just a list of places in Portland my wife and I enjoyed when we lived there. (We moved to Denver last fall, so the site is on permanent hiatus.)
As Zach noted shortly after launching:
I’ve already received multiple reports of people updating their sites to be even faster to try and break into the Top Eleven results.
I'm not a competitive person and Eats & Drinks is no longer in active development, but I started to feel bad watching my little site drop in the rankings week after week, so I dusted things off and got to work. Here's what I've done so far to improve on the site's original SpeedIndex
median score of about 1887
†.
First Pass
I know very little about web performance strategies and have absolutely zero experience improving an actual site's actual performance, but I do listen to ShopTalk Show! I knew my first order of business should be to move some things off the critical rendering path.
Because the site is only one page, I originally defined its CSS internally using a single <style>
tag up in the <head>
block. I also bring in two fonts in a few different weights using a third-party stylesheet from Typekit:
<link rel="stylesheet" href="https://use.typekit.net/lsz1tsk.css">
{% set css %}
{% include "css/index.css" %}
{% include "css/header.css" %}
{% include "css/footer.css" %}
{% include "css/location.css" %}
{% endset %}
<style>{{ css | safe }}</style>
</head>
I figured I could safely defer <footer>
styling as well as font loading by moving them to the very end of <body>
:
<link rel="stylesheet" href="https://use.typekit.net/lsz1tsk.css">
{% set css %}
{% include "css/footer.css" %}
{% endset %}
<style>{{ css | safe }}</style>
</body>
There are tools to fine-tune and automatically identify which styles fall on the critical path, but this seemed like a good place to start. Did it help the following week's ranking?
Performance Rank #74 ↑ 46
Sure did! Jumping solidly into the top 100 isn't too shabby for a few lines of work.
Digging In
It turns out that development based on performance ranking is a virtuous cycle, and it's kind of fun to boot. But my method of working (very) asynchronously—make a change, wait until Sunday for the results—felt a little ridiculous, so I started running my local development copy of Eats & Drinks through the Lighthouse CLI to find more areas for improvement.
Putting the "D.I.Y." in font-display
Lighthouse flagged an issue with text rendering:
Ensure text remains visible during webfont load
Unfortunately, Typekit's @font-face
definition explicitly specifies font-display: auto
and they don't provide any support for the new swap
value that instructs supporting browsers to display text in a fallback font until the specified font is ready. (This results in brief period during rendering when no text is visible at all, which makes for a pretty janky experience.)
This felt like an insurmountable hurdle until I realized I could bypass Typekit's entrypoint CSS and host my own version instead, with one important modification:
font-display: swap;
My original idea was to create a drop-in replacement for the Typekit-hosted CSS, swapping in a self-hosted external CSS file for theirs:
<link rel="stylesheet" href="/fonts.css">
But I had a hunch there was still a bit of room for improvement: any external CSS file, whether it's mine or Typekit's, still requires an additional network request. Since I'm now self-hosting the @font-face
CSS, I can load it however I like:
{% set css %}
{% include "css/fonts.css" %}
{% include "css/footer.css" %}
{% endset %}
<style>{{ css | safe }}</style>
</body>
Moving font definitions to the internal CSS alongside <footer>
styling at build time comes in at a median SpeedIndex
score of... 843
🔥 Hot damn!
I don't know for sure how these changes will pan out in this week's rankings, especially since plenty of other folks are busy tuning their sites as well, but I'm optimistic I'll see at least another modest jump 🤞 And even if not, I learned a little bit about how to make sites more performant.
Time Travel with Netlify Deploys
Unfortunately, I didn't grab Lighthouse scores for Eats & Drinks in its original, pre-improvement state. Lucklily, Eats & Drinks is hosted on Netlify! Thanks to their immutable deployments feature, it's easy to quickly run Lighthouse against any point in the site's history.
Just like we can pass in a production URL:
$ lighthouse https://pdx.ashur.cab/ \
--chrome-flags="--headless" \
--only-categories=performance \
--quiet \
--output=json | jq '.audits["speed-index"].numericValue'
834.663
we can also test against the deploy preview URL for any commit we want to investigate:
$ lighthouse https://5e7989ad1d9b3300088ccb67--mystifying-swirles-c1a0c9.netlify.com/ \
--chrome-flags="--headless" \
--only-categories=performance \
--quiet \
--output=json | jq '.audits["speed-index"].numericValue'
1884.9204580008952
😗🤚 P.S. If you work with JSON on the command line even a little, do yourself a favor and add jq to your toolset.