The web is best when it’s fast. A fast web doesn’t just provide a good experience, though that’s certainly important, it also ensures as many people as possible can access it at all. Many people are on slow, unstable connections. By making a site fast, you make it more likely for them to be able to use it.
Some things I already did
I purposefully did not do many of the tasks below until I was able to write this post, because I wanted to measure the impact of each change and be able to explain each step. However, some things that help the performance of this site would’ve been too time-consuming to build “the slow way” first only to completely change them to make them faster later. For example:
- This site is static, meaning that all of the pages are pre-rendered on the server and then served directly. Most websites have many back-and-forth requests with a server before presenting a page; this one does not, which speeds things up considerably. Serving static content typically saves ~500ms of loading time.
- I’m using a webfont for the headings, but the body text uses a font stack that leverages the viewer’s system’s default font, which does not need to be loaded from the server.
- The animations are built with SVG and driven by a ~40 Kb JS library. It would’ve been easier to just include them as gifs or videos, but that would’ve had much larger download costs.
Please see the Colophon for more details.
Snapshot before any improvements
Note: For all of the WebPageTest snapshots, I‘m testing the homepage of the site, three times, on a Motorola G in Chrome with a ‘3G - slow’ connection. The PageSpeed Insight snapshots are ran once, on the homepage.
|Metric||First View||Repeat View|
|Speed Index (Mobile)||2025||511|
|Load Time (Mobile)||8.666s||2.864s|
While developing on my home’s broadband connection, I thought the site felt fairly zippy, but these baseline results make it clear that there’s plenty of room for improvement. Here’s a breakdown of the content being loaded:
The next typical step is reducing the size of requests. That definitely applies here, so let’s dig in.
Reducing the size of images
Before this exercise, the avatar image on the home page was a 639px × 639px, 392 Kb PNG. It has a fixed-width of 150px in the design, so I resized to 300px × 300px, and optimized it while converting to JPEG, which brought it down to 70 Kb.
But I can do more. Not every screen is retina, so I need to serve the smaller, non-retina version by default, then use
srcset to serve the retina version to screens that need it:
<img src="/avatar.jpg" srcset="/email@example.com 2x" alt="Profile picture of Kyle" />
And the result:
|Metric||First View||Repeat View|
|PageSpeed (Mobile)||90/100 (+22)|
|PageSpeed (Desktop)||96/100 (+25)|
|Speed Index (Mobile)||2056 (+1.5%)||498 (-2.5%)|
|Load Time (Mobile)||8.666s (-16.2%)||2.864s (+1.01%)|
I’m not sure why the speed index on the first view and load time on the repeat view both went up slightly, but it’s such a small change that I’m not too concerned. Those oddities aside, this simple optimization resulted in big, easy speed wins.
And here’s the new breakdown:
Note: Even though I’m only testing the homepage, I went ahead and did this exercise for all images on the site.
My CSS is only 6.5 Kb (gzipped). At that size, that’s too small to justify the effort to make it any smaller. There are other reasons being the filesize to make CSS as small as possible, which I’ll cover later in this post.
Absolute vs. perceived performance
So far, I’ve only made changes that affected the page load time/size, which correlates to the absolute page speed. But that’s not all that matters. Arguably, perceived performance matters even more, as that affects how fast your site feels.
For example, check out this filmstrip view of a portion of the experience waiting for the site to load on 3G:
There’s a full 1.5 seconds between the initial content displaying and the webfont displaying. Worse, because I reference the webfont directly in CSS, via
@font-face, there’s a FOIT making the headings completely unreadable until the webfont has finished loading. This is unacceptable.
Preventing a Flash of Invisible Text (FOIT)
I’m going to use FontFaceObserver to instead only apply the webfont to headings after it is loaded. While it is loading, they’ll use the same font stack as the body text, changing the FOIT into a FOUT. In some scenarios, this can provide a poor experience just like a FOIT, but I have a couple things going for me:
- The body text is set in a font that doesn’t have to load at all, meaning it will display correctly immediately. So when the headings also use that font while the web font loads, they’ll match the rest of the site.
- The webfont I’ve chosen for the headings, League Spartan, has a very similar baseline & caps height, so shifts after the webfont loads are minimal.
After making the change, the filmstrip now looks like:
Much better! You can see how the headings now display for a brief moment using the same font as the rest of the text, and then switch to the webfont after it has finished loading.
Clearing the critical path
What you can’t see in the filmstrips above is all that time before anything is visible:
Part of that is because it must download the full CSS file before it displays anything. In other words, the CSS is blocking the critical path, which happens regardless of the file’s size. So even though the size is fairly small, it has an outsized effect on the perceived performance of the site.
I’ll cover my approach for solving that issue (and possibly removing React on the client-side) in a future post.
The dimensions are doubled to account for retina screens. ↩
You should always endeavor to keep your CSS as small and simple as you reasonably can. The effort I’m avoiding here is because my styles are already architected in a way to keep them very small. Any further optimization will require either a lot of manual fine-tuning that could break future updates or an automated process using something like PurifyCSS. I intend to implement the latter in a future post. ↩