Input delay that’s less than 100 milliseconds is typically perceived as instant by users, so the performance goal we recommend (and the numbers I was hoping to see in my analytics) is FID < 100ms for 99% of page loads.
To my surprise, my site’s FID was 254ms at the 99th percentile. And while that’s not terrible, the perfectionist in me just couldn’t let that slide. I had to fix it!
[W]hile I was trying to solve my issue I stumbled upon a pretty interesting performance strategy that I want to share (it’s the primary reason I’m writing this article).
I’m calling the strategy: idle until urgent.
My performance problem
First input delay (FID) is a metric that measures the time between when a user first interacts with your site (for a blog like mine, that’s most likely them clicking a link) and the time when the browser is able to respond to that interaction (make a request to load the next page).
Here’s what I found when doing a performance trace of my site:
So what’s taking so long to run?
Well, if you look at the tails of this flame chart, you won’t see any single functions that are clearly taking up the bulk of the time. Most individual functions are run in less than 1ms, but when you add them all up, it’s taking more than 100ms to run them in a single, synchronous call stack.
Since the problem is all these functions are being run as part of a single task, the browser has to wait until this task finishes to respond to user interaction. So clearly the solution is to break up this code into multiple tasks[.]
A perfect example of a component that really needs to have its initialization code broken up can be illustrated by zooming closer down into this performance trace. Mid-way through the
main()function, you’ll see one of my components uses the Intl.DateTimeFormat API[.]
Creating this object took 13.47 milliseconds!
The thing is, the
Intl.DateTimeFormatinstance is created in the component’s constructor, but it’s not actually used until it’s needed by other components that reference it to format dates. However, this component doesn’t know when it’s going to be referenced, so it’s playing it safe and instantiating the
Int.DateTimeFormatobject right away.
Idle Until Urgent
After spending a lot of time thinking about this problem, I realized that the evaluation strategy I really wanted was one where my code would initially be deferred to idle periods but then run immediately as soon as it’s needed. In other words: idle-until-urgent.
Idle-until-urgent sidesteps most of the downsides I described in the previous section. In the worst case, it has the exact same performance characteristics as lazy evaluation, and in the best case it doesn’t block interactivity at all because execution happens during idle periods.
Generally, all APIs that synchronously provide layout metrics will trigger forced reflow / layout. Read on for additional cases and details.
Screen readers generally follow this order of precedence:
- Look for explicitly set ARIA attributes (roles, states, properties), and in the absence of those:
- Interpret any implicit roles from markup semantics (form elements, paragraphs, lists, etc).
- Read any text available in the markup.
If no discernable semantics exist (think ARIA-less
<span>tags), it skips to #3. So when you omit ARIA, you’re rolling the dice on whether users will understand that your
<ul>is a menu and not simply a list. When you omit ARIA and semantic tags, you’re leaving it up to your users to figure things out (and potentially abandon your product because it doesn’t work).
TIL that all modern browsers now support complex selectors in :not()! 😍
So you can do things like:
.foo :not(.foo .foo *)to match things inside one .foo wrapper but not two
.container :not(.content *)to get simple (shallow) “donut scope”
Weekly Timber is a client of mine that provides logging services in central Wisconsin. For them, a fast website is vital. Their business is located in Waushara County, and like many rural stretches in the United States, network quality and reliability isn’t great.
Wisconsin has farmland for days, but it also has plenty of forests. When you need a company that cuts logs, Google is probably your first stop. How fast a given logging company’s website is might be enough to get you looking elsewhere if you’re left waiting too long on a crappy network connection.
I initially didn’t believe a Service Worker was necessary for Weekly Timber’s website. After all, if things were plenty fast to start with, why complicate things? On the other hand, knowing that my client services not just Waushara County, but much of central Wisconsin, even a barebones Service Worker could be the kind of progressive enhancement that adds resilience in the places it might be needed most.
The first Service Worker I wrote for my client’s website—which I’ll refer to henceforth as the “standard” Service Worker—used three well-documented caching strategies:
- Serve static assets out of
CacheStorageif available. If a static asset isn’t in
CacheStorage, retrieve it from the network, then cache it for future visits.
- For HTML assets, hit the network first and place the HTML response into
CacheStorage. If the network is unavailable the next time the visitor arrives, serve the cached markup from
These are neither new nor special strategies, but they provide two benefits:
- Offline capability, which is handy when network conditions are spotty.
- A performance boost for loading static assets.
A better, faster Service Worker
The web loves itself some “innovation,” which is a word we equally love to throw around. To me, true innovation isn’t when we create new frameworks or patterns solely for the benefit of developers, but whether those inventions benefit people who end up using whatever it is we slap up on the web. The priority of constituencies is a thing we ought to respect. Users above all else, always.
There are certainly other challenges, but it’ll be up to you to weigh the user-facing benefits versus the development costs. In my opinion, this approach has broad applicability in applications such as blogs, marketing websites, news websites, ecommerce, and other typical use cases.
All in all, though, it’s akin to the performance improvements and efficiency gains that you’d get from an SPA. Only the difference is that you’re not replacing time-tested navigation mechanisms and grappling with all the messiness that entails, but enhancing them. That’s the part I think is really important to consider in a world where client-side routing is all the rage.