But if you’re looking for gains on the CSS side of things, Patrick has a nice way of sniffing out your most expensive selectors using Edge DevTools:
Crack open DevTools.
Head to the Performance Tab.
Make sure you have the “Enable advanced rendering instrumentation” option enabled. This tripped me up in the process.
Record a page load.
Open up the “Bottom-Up” tab in the report.
Check out your the size of your recalculated styles.
From here, click on one of the Recalculated Style events in the Main waterfall view and you’ll get a new “Selector Stats” tab. Look at all that gooey goodness!
Now you see all of the selectors that were processed and they can be sorted by how long they took, how many times they matched, the number of matching attempts, and something called “fast reject count” which I learned is the number of elements that were easy and quick to eliminate from matching.
A lot of insights here if CSS is really the bottleneck that needs investigating. But read Patrick’s full post over on the Microsoft Edge Blog because he goes much deeper into the why’s and how’s, and walks through an entire case study.
I have a handful of good links to articles about performance that are burning a hole in my bookmarks folder, and wanna drop them here to share.
The new WebPageTest website design
From fonts to SVG: an icon migration strategy— Erwin Hofman notes that he was using icon fonts out of sheer convenience, but that there are loads of reasons not to use them. He provides details on his new strategy for using icons, which is based on the <use> technique. Five years later, I’m still a big fan of just dunking the <svg> in the HTML where you need them. It’s just an HTML partial like any other.
Next.js Performance: Making a Fast Framework Even Faster— Ben Schwarz says that Next.js is already a pretty fast framework in that it does smart things that keep even React powered pages snappy. But performance isn’t something that can be left entirely to a framework. You have to do work. Fortunately, Next.js has some pretty handy helpers for things like dynamically (lazily) loading components, deferring scripts, optimizing images, and more.
Redirect Liquidation— Tim Vereecke covers a fascinating technique where instead of redirecting an old URL to a new URL, you let the old URL load, dynamically load the new content, then history.replaceState the old URL with the new URL. It’s faster, but just don’t do it for bots.
Performant A/B Testing with Cloudflare Workers— Philip Walton covers how A/B testing is a little trickier on static sites than server-backed sites, but you can pull it off (performantly) thanks to Cloudflare Workers which can manipulate HTML before it hits the browser, much like a Service Worker except at the edge rather than at the client. Save a cookie and you can maintain users in their proper groups.
A Unified Theory of Web Performance— Alex Russell tries to answer Tanner Hodges’ call for actually defining web performance. It’s one of those things that seems obvious (like it’s clear when certain things help and hurt web performance) but actually defining it is tricky. And not just defining it in terms of specific metrics (even that is tricky), but answer questions like: What are this discipline’s guiding principles?What does it look like to practice web performance? How do we do it?
Unveiling the new WebPageTest UI— I absolutely love seeing WebPageTest’s design evolve and improve. It’s one of those products that’s clearly a best-in-class tool for performance practitioners, yet had a prettttty gnarly design for a lot of years. This is way better. It’s like when Google Fonts finally got a redesign and the broad community let out a collective exhale of appreciation.
Caching Header Best Practices— Simon Hearne’s dissertation on Cache headers. When I first started caring about web performance, this was, like, the main thing. If you incorrectly send Cache headers, users might be re-downloading a file over and over unnecessarily when they don’t need to, which is about the worst thing possible. I’m glad to see headers keep getting attention and re-imagined as the web evolves.
Font Awesome is an incredibly popular icon library. Unfortunately, it’s somewhat easy to use in a way that results in less-than-ideal performance. By subsetting Font Awesome, we can remove any unused glyphs from the font files it provides. This will reduce the number of bytes transmitted over the wire, and improve performance.
Let’s subset fonts together in a Font Awesome project to see the difference it makes. As we go, I’ll assume you’re importing the CSS file Font Awesome provides, and using its web fonts to display icons.
Let’s set things up
For the sake of demonstration, I have nothing but an HTML file that imports Font Awesome’s base CSS file. To get a reasonable sample of icons, I’ve listed out each one that I use on one of my side projects.
Here’s what our HTML file looks like in the browser before subsetting fonts:
Here’s a look at DevTool’s Network tab to see what’s coming down.
Now let’s see how many bytes our font files take to render all that.
Here’s our base case
We want to see what the most straightforward, least performant use of Font Awesome looks like. In other words, we want the slowest possible implementation with no optimization. I’m importing the all.min.css file Font Awesome provides.
As we saw above, the gzipped file weighs in at 33.4KB, which isn’t bad at all. Unfortunately, when we peek into DevTool’s Font tab, things get a little worse.
Yikes. 757KB just for font files. For 54 icons.
While font files are not as expensive a resource for your browser to handle as JavaScript, those are still bytes your browser needs to pull down, just for some little icons. Consider that some of your users might be browsing your site on mobile, away from a strong or fast internet connection.
First attempt using PurifyCSS
Font Awesome’s main stylesheet contains definitions for literally thousands of icons. But what if we only need a few dozen at most? Surely we could trim out the unneeded stuff?
There are many tools out there that will analyze your code, and remove unused styles from a stylesheet. I happen to be using PurifyCSS. While this library isn’t actively maintained anymore, the idea is the same, and in the end, this isn’t the solution we’re looking for. But let’s see what happens when we trim our CSS down to only what’s needed, which we can do with this script:
And when we load that newly built CSS file, our CSS bytes over the wire drop quite a bit, from 33KB to just 7.1KB!
But unfortunately, our other Font Awesome font files are unchanged.
What happened? PurifyCSS did its job. It indeed removed the CSS rules for all the unused icons. Unfortunately, it wasn’t capable of reaching into the actual font files to trim down the glyphs, in addition to the CSS rules.
If only there was a tool like PurifyCSS that handles font files…
Subsetters to the rescue!
There are, of course, tools that are capable of removing unused content from font files, and they’re called subsetters. A subsetter analyzes your webpage, looks at your font files, and trims out the unused characters. There are a bunch of tools for subsetting fonts out there, like Zach Leatherman’s Glyphhanger. As it turns out, subsetting Font Awesome is pretty straightforward because it ships its own built-in subsetters. Let’s take a look.
Subsetting fonts automatically
The auto subsetting and manual subsetting tools I’m about to show you require a paid Font AwesomePro subscription.
Font Awesome allows you to set up what it calls kits, which are described in the Font Awesome docs as a “knapsack that carries all the icons and awesomeness you need in a neat little lightweight bundle you can sling on the back of your project with ease.” So, rather than importing any and every CSS file, a kit gives you a single script tag you can add to your HTML file’s <head>, and from there, the kit only sends down the font glyphs you actually need from the font file.
Creating a kit takes about a minute. You’re handed script tag that looks something like this:
When the script loads, we now have no CSS files at all, and the JavaScript file is a mere 4KB. Let’s look again at the DevTools Fonts tab to see which font files are loaded now that we’ve done some subsetting.
We’ve gone from 757KB down to 331KB. That’s a more than 50% reduction. But we can still do better than that, especially if all we’re rendering is 54 icons. That’s where Font Awesome’s manual font subsetter comes into play.
Subsetting fonts manually
Wouldn’t it be nice if Font Awesome gave us a tool to literally pick the exact icons we wanted, and then provide a custom build for that? Well, they do. They don’t advertise this too loudly for some reason, but they actually have a desktop application exactly for subsetting fonts manually. The app is available to download from their site — but, like the automatic subsetter, this app requires a paid Font Awesome subscription to actually use.
Search the icons, choose the family, add what you want, and then click the big blue Build button. That’s really all it takes to generate a custom subset of Font Awesome icons.
Once you hit the button, Font Awesome will ask where it should save your custom build, then it dumps a ZIP file that contains everything you need. In fact, the structure you’ll get is exactly the same as the normal Font Awesome download, which makes things especially simple. And naturally, it lets you save the custom build as a project file so you can open it back up later to add or remove icons as needed.
We’ll open up DevTools to see the final size of the icons we’re loading, but first, let’s look at the actual font files themselves. The custom build creates many different types, depending on what your browser uses. Let’s focus on the .woff2 files, which is what Chrome loads. The same light, regular, duotone, solid, and brand files that were there before are still in place, except this time no file is larger than 5KB… and that’s before they’re gzipped!
And what about the CSS file? It slims down to just 8KB. With gzip, it’s only 2KB!
Here’s the final tally in DevTools:
Before we go, take a quick peek at those font filenames. The fa-light-300.woff2 font file is still there, but the others look different. That’s because I’m usingVite here, and it decided to automatically inline the font files into the CSS, since they’re so tiny.
That’s why our CSS file looks a little bigger in the DevTools Network tab than the 2KB we saw before on disk. The tradeoff is that most of those font “files” from above aren’t files at all, but rather Base64-encoded strings embedded right in this CSS file, saving us additional network requests.
All that said, Vite is inlining many different font formats that the browser will never use. But overall it’s a pretty small number of bytes, especially compared to what we were seeing before.
Before leaving, if you’re wondering whether that desktop font subsetting GUI tool comes in a CLI that can integrate with CI/CD to generate these files at build time, the answer is… not yet. I emailed the Font Awesome folks, and they said something is planned. That’ll allow users to streamline their build process if and when it ships.
As you’ve seen, using something like Font Awesome for icons is super cool. But the default usage might not always be the best approach for your project. To get the smallest file size possible, subsetting fonts is something we can do to trim what we don’t need, and only serve what we do. That’s the kind of performance we want, especially when it comes to loading fonts, which have traditionally been tough to wrangle.
Does shadow DOM improve style performance? — Nolan Lawson covers how, because of the inherent encapsulation of the shadow DOM, the styling gets applied a bit faster than it would if those styling rules were relevant to the entire page. But as ever, it depends, and it turns out that classes and IDs are actually faster outside of it (?!), so if you just style with those, that’s the way to go.
HTML-first, JavaScript last: the secret to web speed! — Miško Hevery is talking about Qwik and one of the things that makes it fast. I don’t know much about Qwik, but it looks like a JavaScript framework that is pretty SSR-friendly as it keeps state directly in the HTML/DOM (e.g. div ::app-state="./AppState" app-state:1234="{count: 321}">), which you would think is slow, but apparently isn’t.
New HTTP standards for caching on the modern web — Tim Perry gets into two brand new (proposed) caching headers: Cache-Status and Targetted Cache-Control. “These are designed to update HTTP standards to match the reality of the CDN-powered web that exists today, creating specifications that formalize existing practices from popular CDNs.” Feels like good stuff where web standards can come in and fix up a gnarly situation.
Optimizing resource loading with Priority Hints — Leena Sohoni, Addy Osmani, and Patrick Meenan talk about the Priority Hints…. uh… I’d call it an API except it’s really just an importance attribute in HTML, or a JavaScript param in other APIs. Here’s the draft spec. It’s one of those things, like responsive images with srcset/sizes or will-change, where you, the-author, know more than the browser does and allows you to give the browser information it needs to perform better.
Have Core Web Vitals made the web faster? — CWVs were announced in May 2020 and then we were told by May 2021 they would become a factor in SEO. Barry Pollard checks in to see if that big carrot (or stick?) has enticed us all to make sites faster en masse. The answer is complicated because the way CWVs are measured has changed in that time. Some measurements say yes, things are better. But if you use steady alternate measurements from before/after, things look worse. I all depends on what slice of the web you’re looking at.
Improving performance with Islands Architecture and PostCSS — Astro has a cool feature for components—like <Sidebar client:media={"(min-width: 768px)"} />—where that element isn’t loaded at all unless that media query matches. I might argue that’s only a good idea if it’s a heavy component or has JavaScript it requires. I say that because it seems like using it for something like static HTML would actually offset any potential savings, as extra JavaScript has to run to test the media conditions and then conditionally fetch assets. But anyway, Oliver Turner demonstrates how to share the media queries between your JavaScript and CSS (via PostCSS).
Image Optimizer — This is an Electron-powered (but macOS only) drag-and-drop image optimizer. Like ImageOptim, I suppose. Both are free.
Measuring things is great. They say what you only fix what you measure. Raygun is great at measuring websites. Measuring performance, measuring errors and crashes, measuring code problems.
You know what’s even better than measuring? Having a system in place to notify you when anything significant happens with those measurements. That’s why Raygun now has powerful alerting.
Let’s look at some of the possibilities of alerts you can set up on your website so that you’re alerted when things go wrong.
Alert 1) Spike in Errors
In my experience, when you see a spike in errors being thrown in your app, it’s likely because a new release has gone to production, and it’s not behaving how you expected it to.
You need to know now, because errors like this can be tricky. Maybe it worked just fine in development, so you need as much time as you can get to root out what the problem is.
Creating a customized alert situation like this in Raygun is very straightforward! Here’s a quick video:
Alert 2) Critical Error
You likely want to be keeping an eye on all errors, but some errors are more critical than others. If a user throws an error trying to update their biography to a string that contains an emoji, well that’s unfortunate and you want to know about it so you can fix it. But if they can’t sign up, add to cart, or check out — well, that’s extra bad, and you need to know about it instantly so you can fix it as immediately as possible. If your users can’t do the main thing they are on your website to do, you’re seriously jeopardizing your business.
With Raygun Alerting, there are actually a couple ways to set this up.
Set up the alert to watch for an Error Message containing any particular text
(and/or) Set up the alert to watch for a particular tag
Error Message text is a nice catch-all as you should be able to catch anything with that. But tagging is more targetted. These tags are of your own design, as you send them over yourself from your own app. For example in JavaScript, say you performed some mission-critical operation in a try/catch block. Should the catch happen, you could send Raygun an event like:
rg4js('send', { error: e, tags: ['signup', 'mission_critical']; });
Then create alerts based on those tags as needed.
Alert 3) Slow Load Time
I’m not sure most people think about website performance tracking as something you tie real time alerting to, but you should! There is no reason a websites load time would all the sudden nose dive (e.g. change from, say 2 seconds to 5 seconds), unless something has changed. So if it does nose dive, you should be alerted right away, so you can examine recent changes and fix it.
With Raygun, an alert like this is extremely simple to set up. Here’s an example alert set up to watch for a certain load time threshold and email if there is ever a 10 minute time period in which loading times exceed that.
Setting up the alert in Raygun
Email notification of slowness
If you don’t want to be that aggressive to start with loading time, try 4 seconds. That’s the industry standard for slow loading. If you never get any alerts, slowly notch it down over time, giving you and your team progressively more impressive loading times to stay vigilant about.
Aside from alerts, you’ll also get weekly emails giving you an overview of performance issues.
Alert 4) Core Web Vitals
The new gold-standard web performance metrics are Core Web Vitals (which we’ve written about how Raygun helps with before) (CWV) because they measure things that really matter to users, as well as are an SEO ranking factor for Google. Those are two big reasons to be extra careful with them and set up alerts if your website breaks acceptable thresholds you set up.
For example, CLS is Culumative Layout Shift. Google tells us CLS under 0.1 is good and above 0.25 is bad. So why don’t we shoot for staying under 0.1?
Here we’ve got an alert where if the CLS creeps up over 0.1, we’ll be alerted. Maybe we accidentally added some new content to the site (ads?) that arrive after the page loads and push content around. Perhaps we’ve adjusted a layout in a way that makes things more shifty than they were. Perhaps we’ve updated our custom fonts such that when the load they cause shifting. If we’re alerted, we can fix it the moment we’re aware of it so the negative consequences don’t stick around.
Conclusion
For literally everything that you measure that you know is important to you, there should be an alerting mechanic in place. For anything website performance or error tracking related, Raygun has a perfect solution.
This is some bonafide CSS trickery from Harry that gives you some generic performance advice based on what it sees in your <head> element.
First, it’s possible to make a <style> block visible like any other element by changing the display away from the default of none. It’s a nice little trick. You can even do that for things in the <head>, for example…
head, head style, head script { display: block; }
From there, Harry gets very clever with selectors, determining problematic situations from the usage and placement of certain tags. For example, say there is a <script> that comes after some styles…
Well, that’s bad, because the script is blocked by CSS likely unnecessarily. Perhaps some sophisticated performance tooling software could tell you that. But you know what else can? A CSS selector!
head [rel="stylesheet"]:not([media="print"]):not(.ct) ~ script, head style:not(:empty) ~ script { }
That’s kinda like saying head link ~ script, but a little fancier in that it only selects actual stylesheets or style blocks that are truly blocking (and not itself). Harry then applies styling and pseudo-content to the blocks so you can use the stylesheet as a visual performance debugging tool.
That’s just darn clever, that. The stylesheet has loads of little things you can test for, like attributes you don’t need, blocking resources, and elements that are out of order.
HTTP Caching is a Superpower — Hugh Haworth covers how the Cache-Control header is an awfully potent ingredient in web performance. I mis-read the title at first and was waiting to read about HTML caching. Hugh covers it a bit (like how you’d need to be careful doing so on something like a forum, where the content on pages changes rapidly), but I find it something that’s generally under-talked-about. As in, generally, people just don’t cache HTML at all, because it changes the most and it’s risky being the parent of most other cache. I only do it now because Cloudflare handles it.
Why it’s okay for web components to use frameworks — I admit it rubs me the wrong way to think of web components as needing a framework at all, let alone being OK with a hogepodge of random frameworks. But Nolan Lawson digs into the nuance here. A couple of kilobytes that might be lazy loaded/code-split out is not that big of a deal, especially since the frameworks might be optimized for runtime performance.
web-vitals-element — An npm packages from Stefan Judis that boots up a <web-vitals> web component showing your CLS, FID, LCP, and BVD (demo). Sorta weird it won’t build on Skypack.
Top 10 performance pitfalls — I probably wouldn’t have guessed any of the things Jake and Surma cover in this top 10 list. My guess is that the low hanging fruit of performance either becomes a bunch of non-issues through technological improvements, or it is generally handled by site owners and hosts and, thus, a new set of problems become the top offenders.
How to Eliminate Render-Blocking Resources: a Deep Dive — Sia Karamalegos: Render-blocking resources are files that ‘press pause’ on the critical rendering path. They interrupt one or more of the steps. You should be super aware of anything that is render-blocking and only render-block on purpose (like critical CSS) rather than letting it happen by accident.
How we reduced Next.js page size by 3.5x and achieved a 98 Lighthouse score — Colin Armstrong covers how dynamically loading assets, reducing the size of assets, and using responsive images goes along way. Fortunately, the tooling to diagnose performance problems and the tools for solving them are largely build into Next.js. The bit about PurgeCSS and Tailwind seems extra pertinant here. I think if you aren’t using PurgeCSS to remove unused Tailwind selectors (or the JIT compiler to only create the selectors you need), you’re basically doing Tailwind wrong. Shipping 350KB of CSS instead of the 10KB you need is not OK.
Improving responsiveness in text inputs — Nolan Lawson covers how to prevent blocking the input event with “expensive” main thread work, using requestIdleCallback to batch UI updates.
Vector? Raster? Why Not Both! — Zach gets the best possible file size by splitting a graphic into two parts: An SVG (vector) for the things SVG does well, and a super-optimized raster graphic (ideally AVIF) for the things it does well, then plopping them on top of one another.
Making GitHub’s new homepage fast and performant — Tobias Ahlin describes how the scrolling effects are done more performantly thanks to IntersectionObserver and the fact that it avoids the use of methods that trigger reflows, like getBoundingClientRect. Also, WebP + SVG masks!
Everything we know about Core Web Vitals and SEO — Simon Hearne covers why everyone is so obsessed with CWV right now: SEO. Simon says something I’ve heard a couple of times: The Page Experience Update is more of a carrot approach than stick — there is no direct penalty for failing to meet Google’s goals. That is, you aren’t penalized for poor CWV, but are given a bonus for good numbers. But if everyone around you is getting that bonus except you, isn’t that the same as a penalty?
Setting up Cloudflare Workers for web performance optimisation and testing — Matt Hobbs starts with a 101 intro on setting up a Cloudflare Worker, using it to intercept a CSS file and replace all the font-family declarations with Comic Sans. Maybe that will open your eyes to the possibilities: if you can manipulate all assets like HTML, CSS, and JavaScript, you can force those things into doing more performant things.
Now THAT’S What I Call Service Worker!— Jeremy Wagner sets up a “Streaming” Service Worker that caches common partials on a website (e.g. the header and footer) such that the people of Waushara County, Wisconsin, who have slow internet can load the site somewhere in the vicinity of twice as fast. This is building upon Philip Walton’s “Smaller HTML Payloads with Service Workers” article.
Who has the fastest F1 website in 2021? — Jake Archibald’s epic going-on-10-part series analyzing the performance of F1 racing websites (oh, the irony). Looks like Red Bull is in the lead so far with Ferarri trailing. There is a lot to learn in all these, and it’s somewhat cathartic seeing funny bits like, Their site was slow because of a 1.8MB blocking script, but 1.7MB of that was an inlined 2300×2300 PNG of a horse that was only ever displayed at 20×20. Also, I don’t think I knew that Jake was the original builder of Sprite Cow! (Don’t use that because it turns out that sprites are bad.)
Real-world CSS vs. CSS-in-JS performance comparison — Tomas Pustelnik looks at the performance implications of CSS-in-JS. Or, as I like to point out: CSS-in-React, as that’s always what it is since all the other big JavaScript frameworks have their own blessed styling solutions. Tomas didn’t compare styled-components to hand-written vanilla CSS, but to Linaria, which I would think most people still think of as CSS-in-JS — except that instead of bundling the styles in JavaScript, it outputs CSS. I agree that, whatever a styling library does for DX, producing CSS seems like the way to go for production. Yet another reason I like css-modules. Newer-fancier libs are doing it too.
The Case of the 50ms request — Julia Evans put together this interactive puzzle for trying to figure out why a server request is taking longer than it should. More of a back-end thing than front-end, but the troubleshooting steps feel familiar. Try it on your machine, try it on my machine, see what the server is doing, etc.
When there is CSS available for a page, whether it’s inline or an external stylesheet, the browser delays rendering until the CSS is parsed. This is because pages without CSS are often unusable.
The browser has to wait until the CSS is both downloaded and parsed to show us that first rendering of the page, otherwise browsing the web would be a terribly visually jerky to browse. We’d probably write JavaScript to delay page rendering on purpose if that’s how the native web worked.
So how do you improve it? The classics like caching, minification, and compression help. But also, shipping less of it, and only loading the bit you need and the rest after the first render.
It’s entirely about how and how much CSS you load, and has very little to do with the contents of the the CSS.
Alex Russell made some interesting notes about performance and how it impacts folks on mobile:
[…] CPUs are not improving fast enough to cope with frontend engineers’ rosy resource assumptions. If there is unambiguously good news on the tooling front, multiple popular tools now include options to prevent sending first-party JS in the first place (Next.js, Gatsby), though the JS community remains in stubborn denial about the costs of client-side script. Hopefully, toolchain progress of this sort can provide a more accessible bridge as we transition costs to a reduced-script-emissions world.
A lot of the stuff I read when it comes to performance is focused on America, but what I like about Russell’s take here is that he looks at a host of other countries such as India, too. But how does the rollout of 5G networks impact performance around the world? Well, we should be skeptical of how improved networks impact our work. Alex argues:
5G looks set to continue a bumpy rollout for the next half-decade. Carriers make different frequency band choices in different geographies, and 5G performance is heavily sensitive to mast density, which will add confusion for years to come. Suffice to say, 5G isn’t here yet, even if wealthy users in a few geographies come to think of it as “normal” far ahead of worldwide deployment
This is something I try to keep in mind whenever I’m thinking about performance: how I’m viewing my website is most likely not how other folks are viewing it.