Optimize First Input Delay

How to respond faster to user interactions.

I clicked but nothing happened! Why can't I interact with this page? 😢

First Contentful Paint (FCP) and Largest Contentful Paint (LCP) are both metrics that measure the time it takes for content to visually render (paint) on a page. Although important, paint times do not capture load responsiveness: or how quickly a page responds to user interaction.

First Input Delay (FID) is a Core Web Vitals metric that captures a user's first impression of a site's interactivity and responsiveness. It measures the time from when a user first interacts with a page to the time when the browser is actually able to respond to that interaction. FID is a field metric and cannot be simulated in a lab environment. A real user interaction is required in order to measure the response delay.

Good fid values are 2.5 seconds, poor values are greater than 4.0 seconds and anything in between needs improvement

To help predict FID in the lab, we recommend Total Blocking Time (TBT). They measure different things, but improvements in TBT usually correspond to improvements in FID.

The main cause of a poor FID is heavy JavaScript execution. Optimizing how JavaScript parses, compiles, and executes on your web page will directly reduce FID.

Heavy JavaScript execution

The browser cannot respond to most user input while it's executing JavaScript on the main thread. In other words, the browser can't respond to user interactions while the main thread is busy. To improve this:

Break up Long Tasks

If you've already attempted to reduce the amount of JavaScript that loads on a single page, it can be useful to break down long-running code into smaller, asynchronous tasks.

Long Tasks are JavaScript execution periods where users may find your UI unresponsive. Any piece of code that blocks the main thread for 50 ms or more can be characterized as a Long Task. Long Tasks are a sign of potential JavaScript bloat (loading and executing more than a user may need right now). Splitting up long tasks can reduce input delay on your site.

Long Tasks in Chrome DevTools
Chrome DevTools visualizes Long Tasks in the Performance Panel

FID should improve noticeably as you adopt best practices like code-splitting and breaking up your Long Tasks. While TBT is not a field metric, it's useful for checking progress towards ultimately improving both Time To Interactive (TTI) and FID.

Optimize your page for interaction readiness

There are a number of common causes for poor FID and TBT scores in web apps that rely heavily on JavaScript:

First-party script execution can delay interaction readiness

  • JavaScript size bloat, heavy execution times and inefficient chunking can slow down how soon a page can respond to user input and impact FID, TBT, and TTI. Progressive loading of code and features can help spread this work out and improve interaction readiness.
  • Server-side rendered apps may look like they're getting pixels painted on the screen quickly, but beware of user interactions being blocked by large script executions (e.g. re-hydration to wire up event listeners). This can take several hundred milliseconds, sometimes even seconds, if route-based code splitting is being used. Consider shifting more logic server-side or generating more content statically during build time.

Below are the TBT scores before and after optimizing first-party script loading for an application. By moving costly script loading (and execution) for a non-essential component off the critical path, users were able to interact with the page much sooner.

Improvements in TBT score in Lighthouse after optimizing the first-party script.

Data-fetching can impact many aspects of interaction readiness

  • Waiting on a waterfall of cascading fetches (e.g. JavaScript and data fetches for components) can impact interaction latency. Aim to minimize a reliance on cascading data fetches.
  • Large inline datastores can push out HTML parsing time and impact both paint and interaction metrics. Aim to minimize how much data needs to be post-processed on the client-side.

Third-party script execution can delay interaction latency too

  • Many sites include third-party tags and analytics which can keep the network busy and make the main thread periodically unresponsive, impacting interaction latency. Explore on-demand loading of third-party code (e.g. maybe don't load those below-the-fold ads until they're scrolled closer to the viewport).
  • In some cases, third-party scripts can pre-empt first-party ones in terms of priority and bandwidth on the main thread, also delaying how soon a page is interaction-ready. Attempt to prioritize loading what you believe offers the greatest value to users first.

Use a web worker

A blocked main thread is one of the main causes of input delay. Web workers make it possible to run JavaScript on a background thread. Moving non-UI operations to a separate worker thread can cut down main thread blocking time and consequently improve FID.

Consider using the following libraries to make it easier to use web workers on your site:

  • Comlink: A helper library that abstracts postMessage and makes it easier to use
  • Workway: A general purpose web worker exporter
  • Workerize: Move a module into a web worker

Reduce JavaScript execution time

Limiting the amount of JavaScript on your page reduces the amount of time that the browser needs to spend executing JavaScript code. This speeds up how fast the browser can begin to respond to any user interactions.

To reduce the amount of JavaScript executed on your page:

  • Defer unused JavaScript
  • Minimize unused polyfills

Defer unused JavaScript

By default all JavaScript is render-blocking. When the browser encounters a script tag that links to an external JavaScript file, it must pause what it's doing and download, parse, compile, and execute that JavaScript. Therefore you should only load the code that's needed for the page or responding to user input.

The Coverage tab in Chrome DevTools can tell you how much JavaScript is not being used on your web page.

The Coverage tab.

To cut down on unused JavaScript:

  • Code-split your bundle into multiple chunks
  • Defer any non-critical JavaScript, including third-party scripts, using async or defer

Code-splitting is the concept of splitting a single large JavaScript bundle into smaller chunks that can be conditionally loaded (also known as lazy loading). Most newer browsers support dynamic import syntax, which allows for module fetching on demand:

import('module.js').then((module) => {
  // Do something with the module.
});

Dynamically importing JavaScript on certain user interactions (such as changing a route or displaying a modal) will make sure that code not used for the initial page load is only fetched when needed.

Aside from general browser support, dynamic import syntax can be used in many different build systems.

  • If you use webpack, Rollup, or Parcel as a module bundler, take advantage of their dynamic import support.
  • Client-side frameworks, like React, Angular, and Vue provide abstractions to make it easier to lazy-load at the component-level.

Aside from code-splitting, always use async or defer for scripts that are not necessary for critical-path or above-the-fold content.

<script defer src="…"></script>
<script async src="…"></script>

Unless there is a specific reason not to, all third-party scripts should be loaded with either defer or async by default.

Minimize unused polyfills

If you author your code using modern JavaScript syntax and reference modern browsers APIs, you will need to transpile it and include polyfills in order for it to work in older browsers.

One of the main performance concerns of including polyfills and transpiled code in your site is that newer browsers shouldn't have to download it if they do not need it. To cut down on the JavaScript size of your application, minimize unused polyfills as much as possible and restrict their usage to environments where they're needed.

To optimize polyfill usage on your site:

  • If you use Babel as a transpiler, use @babel/preset-env to only include the polyfills needed for the browsers you plan on targeting. For Babel 7.9, enable the bugfixes option to further cut down on any unneeded polyfills
  • Use the module/nomodule pattern to deliver two separate bundles (@babel/preset-env also supports this via target.esmodules)

    <script type="module" src="modern.js"></script>
    <script nomodule src="legacy.js" defer></script>
    

    Many newer ECMAScript features compiled with Babel are already supported in environments that support JavaScript modules. So by doing this, you simplify the process of making sure that only transpiled code is used for browsers that actually need it.

Developer tools

A number of tools are available to measure and debug FID:

With thanks to Philip Walton, Kayce Basques, Ilya Grigorik, and Annie Sullivan for their reviews.