How to simulate a bad First Input Delay (Web Core Vitals) score?

521 views Asked by At

I need to create a webpage that will generate a bad First Input Delay (FID) value.

In case you aren't aware, FID is part of Google's Web Core Vitals.

I want to simulate a bad FID because I am testing a website scanning tool that is supposed to flag a bad FID value. Therefore I want to simulate a bad value on a webpage to make sure it works.

To be clear - I am NOT trying to fix my First Input Delay. I want to create a webpage that gives a bad First Input Delay value on purpose.

But I'm not sure how to do that.

I have a HTML page with <button id="button">Click Me</button>. And in the <head> I have added this script:

<script type="module">
// Get the First Input Delay (FID) Score 
import {onFID} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module';

// Get the button element
const button = document.getElementById('button');

// Add a click event listener to the button
button.addEventListener('click', async () => {
  // Make a delay
  await new Promise((resolve) => setTimeout(resolve, 5000));
  // Print the FID score to the console
  onFID(console.log);
});
</script>

The imported onFID method is what Google uses from Web Vitals to report the FID value.

You can see a live version of the above script here: http://seosins.com/extra-pages/first-input-delay/

But when I click the button, 5000 milliseconds later it only prints a FID of about 3 milliseconds.

The 5000 millisecond delay is not included in the FID value.

Why doesn't it report the FID value as 5003 milliseconds?

When I try to simulate a bad FID value I am doing something wrong.

What could it be?

Update:

As suggested in the comments, I have also tried adding a delay on the server using a Cloudflare Worker. That worker delayed the server response by 5000 milliseconds. But it didn't work, because the FID value was unchanged.

Also I do not think this is the correct approach because FID measures the time from when a user first interacts with your site (i.e. when they click a link, tap on a button, etc) to the time when the browser is actually able to respond to that interaction. While the Cloudflare Worker was only slowing down the initial server response. Therefore I have since removed this experiment from the page.

1

There are 1 answers

4
Inigo On BEST ANSWER

I think you misunderstand what FID is

From web.dev's page on First Input Delay (FID):

What is FID?

FID measures the time from when a user first interacts with a page (that is, when they click a link, tap on a button, or use a custom, JavaScript-powered control) to the time when the browser is actually able to begin processing event handlers in response to that interaction.

and

Gotchas

FID only measures the "delay" in event processing. It does not measure the event processing time itself nor the time it takes the browser to update the UI after running event handlers.

also:

In general, input delay (a.k.a. input latency) happens because the browser's main thread is busy doing something else, so it can't (yet) respond to the user. One common reason this might happen is the browser is busy parsing and executing a large JavaScript file loaded by your app.

Here is my understanding: Actual FID measurement is built into Chrome. The web-vitals library simulates this using browser measurement APIs. The measurement isn't based on when onFID is called; onFID simply sets up a measurement event listener with those APIs. What is measured is the time between when a user clicks on something (e.g. the button) and when its event handler is triggered, not how long that handler takes to complete (see second quote above).

First, we need something that occupies (i.e. blocks) the JS Event Loop

setTimeout does not do that. It just delays when something happens. In the meantime the event loop is free to do other work, e.g. process user input. Instead you need code that does exactly what you're not supposed to do: Do some lengthy blocking CPU-bound work synchronously, thus preventing the event loop from handling other events including user input.

Here is a function that will block a thread for a given amount of time:

function blockThread (millis) {
    let start = Date.now()
    let x = 928342343234
    while ((Date.now() - start) < millis) {
        x = Math.sqrt(x) + 1
        x = x * x
    }
}

or maybe just:

function blockThread (millis) {
    let start = Date.now()
    while ((Date.now() - start) < millis) {
    }
}

Now the question is: When/where do we block the event loop?

Before I reached the understanding above, my first thought was to just modify your approach: block the event loop in the button click event listener. Take your code, remove the async, and call blockThread instead of setting a timer. This runnable demo does that:

// this version of blockThread returns some info useful for logging
function blockThread (millis) {
    let start = Date.now()
    let i = 0
    let x = 928342343234
    while (true) {
        i++
        x = Math.sqrt(x) + 1
        x = x * x
        let elapsed = (Date.now() - start)
        if (elapsed > millis) {
            return {elapsed: elapsed, loopIterations: i}
        }
    }
}

const button = document.getElementById('button');

button.addEventListener('click', () => {
  const r = blockThread(5000)
  console.log(`${r.elapsed} millis elapsed after ${r.loopIterations} loop iterations`)
  console.log('calling onFID')
  window.onFID(console.log)
  console.log('done')
})
<!DOCTYPE html>
<html>

<script type="module">
  import {onFID} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module'
  window.onFID = onFID
</script>

<head>
  <title>Title of the document</title>
</head>

<body>
  <button id='button'>Block for 5000 millis then get the First Input Delay (FID) Score</button>  click me!
</body>

<p>  Notice how the UI (i.e. StackOverflow) will be unresponsive to your clicks for 5 seconds after you press the above button. If you click on some other button or link on this page, it will only respond after the 5 seconds have elapsed</p>

</html>

I'd give the above a try to confirm, but I'd expect it to NOT impact the FID measurement because:

  1. Both your version and mine blocks during the execution of the event handler, but does NOT delay its start.

    What is measured is the time between when a user clicks on something (e.g. the button) and when its event handler is triggered.

    I'm sure we need to block the event loop before the user clicks on an input, and for long enough that it remains blocked during and after that click. How long the event loop remains blocked after the click will be what the FID measures.

  2. I'm also pretty sure we need to import and call onFID before we block the event loop.

    The web-vitals library simulates Chrome's internal measurement. It needs to initialize and attach itself to the browser's measurement APIs as a callback in order for it to be able to measure anything. That's what calling onFID does.

So let's try a few options...

start blocking the event loop while the page is loaded

Looking at the Basic usage instructions for web-vitals I arrived at this:

<!-- This will run synchronously during page load -->
<script>
  import {onFID} from 'web-vitals.js'

  // Setup measurement of FID and log it as soon as it is
  // measured, e.g. after the user clicks the button.
  onFID(console.log)

  // !!! insert the blockThread function declaration here !!!

  // Block the event loop long enough so that you can click
  // on the button before the event loop is unblocked. We are
  // simulating page resources continuing to load that delays
  // the time an input's event handler is ever called.
  blockThread(5000)
</script>

But I suspect that calling blockThread as above will actually also block the page/DOM from loading so you won't even have a button to click until it's too late. In that case:

start blocking after the page is loaded and before the DOMContentLoaded event is triggered    (my bet is on this one)

<script>
  import {onFID} from 'web-vitals.js'
  onFID(console.log)
</script>
<script defer>
  // !!! insert the blockThread function declaration here !!!
  blockThread(5000)
</script>

If that still doesn't work, try this:

start blocking when the DOMContentLoaded event is triggered

<script>
  import {onFID} from 'web-vitals.js'
  onFID(console.log)

  // !!! insert the blockThread function declaration here !!!

  window.addEventListener('DOMContentLoaded', (event) => {
    blockThread(5000)
  });
</script>

Check out this this excellent answer to How to make JavaScript execute after page load? for more variations.


Let me know if none of these work, or which one does. I don't have the time right now to test this myself.