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:
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.
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.