One common complaint around web applications is that they can feel slow or clunky compared with native applications. Sometimes we find sites that can take a long time to load (let’s say longer than 3 seconds) and sometimes those sites feel non-interactive or ‘janky’ when we try to interact with them. Many sites, especially those that leverage JavaScript heavily, can suffer from this problem as JavaScript parsing, compiling and execution can be blocking.
In modern web development, we have identified many metrics for determining the experience a user receives when viewing a page. Some common metrics that we often hear about are:
- Time to first byte: how quickly it takes for the server to send the first payload to the client
- Time to first meaningful paint: how quickly it takes to render some meaningful content to the screen
- Time to interactive: how quickly until the site responds smoothly to user interactions
These are all valid ways to measure various parts of the user experience and it makes sense not to try to overemphasise one in particular.
One common part of the user journey in a site or application is the time the user first tries to interact with the page. Similar to how, if we don’t serve a site in a timely manner, users get frustrated, users also get frustrated if we give an illusion of interactivity. If the user goes to tap, click or drag and the app is unresponsive, this could be perceived in a harsh light, potentially causing the user to leave the site or application. This leaves space for another metric, namely First Input Delay (FID for short) which will be the focus of this post.
Defining First Input Delay
First Input Delay is a relatively new metric for measuring web page performance. It was introduced by Google, described by Philip Walton in May 2018. At a high level, we can define First Input Delay as the length of time before a response to the first user interaction on a web page. More explicitly, we can define interactions as the user clicking or using gestures on a part of the application that is controlled via JavaScript (for example, scrolling or zooming would not be an interaction in this strict definition). Generally, we measure this metric in milliseconds as users will perceive an interaction that takes less than 100ms to respond as instantaneous.
Measuring First Input Delay
Although you could measure a first input delay manually from your developer tools, the most effective way to measure FID at scale is to use the first-input-delay library from the Chrome team. The library loops through interaction events and adds a callback to each event. It in that callback it takes the time difference in milliseconds between the timeStamp
of the event (when it fired) and when the browser was able to handle it. This library provides an interface for measuring the delay in the following fashion:
perfMetrics.onFirstInputDelay(function(delay, evt) {
// Do something with the delay, for example send to analytics
});
You will want to inline the first-input-delay
library into your head
of your page as you want to make sure firstly that it isn’t render blocking, but also so that it is available as soon as possible to catch the user interaction. You may also want to inline the actual callback above too for the same reason.
If you don’t want to use analytics or run this in production, one approach could potentially be to run headless Chrome via Puppeteer to try and obtain this metric in a programmatic fashion, although this is an explorative exercise left to the reader.
Who should be considering this metric?
Generally, FID would be valuable to understand for anyone building a site that isn’t static, especially one that uses a large amount of JavaScript. Sites that use server-side rendering (SSR) may also be worth examining the FID of, as they often present the site quickly but then have to hydrate on the client with added JavaScript functionality (event listeners and so forth). Any sites that have large initialisation steps can lead to long FIDs, so these would also be a good candidate. This can be determined by examining the performance tab in your favourite web browser when looking at your page and checking to see what is happening on the main-thread during page load.
Reducing First Input Delay
As web developers, First Input Delay is something we want to keep to a minimum. Some sites will have no First Input Delay as the user may not interact with the site in ways other than scrolling or zooming (which are not covered by FID). Fundamentally, the main thread is predominantly blocked by parsing, compiling and executing JavaScript. On slower devices such as low end mobiles, this time is increased dramatically. A common cause of long FIDs is large JavaScript bundles with long-running initialisation tasks. Although it may not be possible to reduce a given bundle size, we can change the strategy we take in handling its execution. For example, we can break long-running tasks down into smaller tasks (sub 50ms) – allowing the browser to yield to rendering and input handling. Browsers will generally prioritise user interactions over site tasks in a process called input priotizitation so breaking down code into smaller tasks allows the browser the opportunity to prioritise any user inputs. As tasks in JavaScript run till completion, a simple way to break them up is to force a new task using setTimeout
with a 0ms timer. However, there are perhaps better methods than this which we will discuss below.
Philip Walton, author of the original FID blog post, has suggested a development pattern termed ‘Idle Till Urgent’. Here we avoid evaluating our code up front and even lazily waiting to evaluate the code until it’s needed. Instead, we take the approach of leveraging the ability to evaluate code in idle periods. Here the requestIdleCallback
browser API is used, which allows code to run during the time when the browser main thread is not currently working (it is idle). This API is currently supported in Chrome, Firefox and Opera, but there is a shim for unsupported browsers. There is also a Polyfill in the React source code which you can explore if you’re feeling experimental. The trick is if a given value is needed urgently, we cancel the requestIdleCallback
callback and instead calculate the value immediately.
This approach has notably been implemented in two libraries, namely:
- Idlize by Philip Walton
- idle-until-urgent by Josh Duff
Let’s compare how they look (using ES6 syntax) to get values during idle times. In this case, we’re going to format date and time using Intl for the ‘Europe/London’ timezone. Firstly Idlize:
import { IdleValue } from 'idlize/IdleValue.mjs'
const formatter = new IdleValue(() => new Intl.DateTimeFormat('en-US', {
timeZone: 'Europe/London'
}));
const value = formatter.getValue().format(new Date(1537452915210));
Comparing with idle-until-urgent:
import * as makeIdleGetter from 'idle-until-urgent';
const formatter = makeIdleGetter(() => new Intl.DateTimeFormat('en-US', {
timeZone: 'Europe/London'
}));
const value = formatter().format(new Date(1537452915210)); // => '9/20/2018'
As you can see, both libraries take similar approaches to getting values during idle periods with only minor API differences. The main difference of note here is that Idlize has a much wider set of APIs at its disposal, with idle-until-urgent just focusing on getting idle values (makeIdleGetter
). However, idle-until-urgent boasts a smaller bundle size (one-third of the size of defineIdleProperty.mjs after being built) and uses a functional rather than class based approach.
Idlize also has other features we can leverage to better implement the idle-until-urgent pattern—for example, idle task queues. Here, we can add multiple functions to a queue and allow those functions to be executed when the browser is idle. This looks a little something like this:
import { IdleQueue } from 'idlize/IdleQueue.mjs';
const queue = new IdleQueue();
const state = {};
function getWordStatistics(text) {
// Remove punctuation
text.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"")
text = text.toLowerCase();
words = text.split(' ');
const wordCount = {}
let totalLength = 0;
words.forEach((word) => {
totalLength += word.length;
wordCount[word] = wordCount[word] ? wordCount[word] + 1 : 1;
});
return {
averageLength: parseInt(totalLength / words.length),
wordCount
}
}
queue.pushTask(() => {
getWordStatistics(`JavaScript, often abbreviated as JS, is a high-level,
interpreted programming language.
It is a language which is also characterized as dynamic,
weakly typed, prototype-based and multi-paradigm.`);
});
Similar to the idle value approach, we can also run the functions immediately if they are urgently required.
Keeping the main thread idle
In the preceding sections, we’ve expressed that a long First Input Delay is often caused by long-running tasks that don’t yield to the browser to allow rendering and handling of input. What can be done about this? One thing that might help, alongside splitting up long tasks, is running them off the main thread entirely. Here we could use Web Workers for long running tasks to take workloads onto different threads. The benefit here is that work that happens in another thread and doesn’t block the main thread, keeping it free for rendering and handling user input. David East has shown how he leveraged Web Workers to decrease Time-to-Interactive and it is fair to believe that the benefits of Web Workers can be used to reduce FID too.
A core part of keeping FID low is making sure tasks are kept below 50ms run time on the main thread. Web Workers mean that work is happening off the main thread so that it is free to render and respond to user interactions. It is possible to write a Web Worker up front, but sometimes it might be easier to use a library like Greenlet by Jason Miller to inline your Web Workers on the fly. Let’s see how we could take the same interaction we put on the Idle Queue using Idlize and see if there’s a way to shoot this off with Greenlet:
import greenlet from 'greenlet'
const getWordStatistics = greenlet(words => {
text.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"")
text = text.toLowerCase();
words = text.split(' ');
const wordCount = {}
let totalLength = 0;
words.forEach((word) => {
totalLength += word.length;
wordCount[word] = wordCount[word] ? wordCount[word] + 1 : 1;
});
return {
averageLength: parseInt(totalLength / words.length),
wordCount
}
});
Here we have completely moved the execution of getting the average length and word counts over to another thread, meaning we don’t have to wait for idle time to execute it. A common question here will be “isn’t there a cost to transferring data to a Web Worker?” and the answer is yes, but it’s generally less than developers tend to think, with objects with fewer than 1,000 keys taking less than a millisecond on modern laptops. Essentially, we pay a small cost to transfer the data but avoid blocking the main thread in return. This being said, there is a solid limitation here, namely that we can’t do DOM operations (although there is an effort to provide a Web Worker DOM interface from the AMP team). Also, using inlined workers, functions generally have to be pure (free of outer scope variables) to work correctly, as toString
is often used to transfer the function’s code in this approach. We also make have to make our code asynchronous which could be situationally undesirable.
Conclusion
As the amount of JavaScript in our pages has increased on average over time, we need ways to mitigate providing poor user experiences, especially for those on low-end devices. In this post, we have discussed what First Input Delay is—explaining it is a metric for determining the total length of time spent responding to a user’s first input to a page. We then went on to show the first-input-delay library, measuring the FID of a page for our users. We then explored methods and libraries for reducing FID and rounded things off by examining using Web Workers to keep the main thread as idle as possible. Hopefully we have provided an overview of how FID matters in your applications and how you can keep it to a minimum for the end users of your sites and applications.