The main differences are that await-interaction-response uses requestAnimationFrame to wrap the setTimeout so explicitly waits for the next frame, thereby ensuring the INP end point is reached. The yieldToMain function however simply yields and leaves the choice with the browser on whether to paint a frame or not, therefore it does not guarantee a frame will happen and the browser will not instead process another task.
For example, if you have a task that takes 500ms to complete and you call these functions to split it into two tasks. If the split happens early (say after 2ms, leaving a follow up task of 498ms), the browser may decide not to render a frame immediately as usually it tries to render a frame every 16ms and there are still 14ms left. So it might decide to take another task from the queue and if that next task is the 498ms task (because the browser doesn't know in advance how much for it will be), then you've had seemingly no benefit from splitting and you still have an INP of ~500ms. With await-interaction-response you're guaranteeing a frame will happen and have an INP of 2ms, and then the 498ms is in the next frame.
So sounds like await-interaction-response is better right? Well yes... but also no. As always, it depends.
From a perspective of looking to improve INP for this frame, it is better. However, the reality is that the browser is likely to render another frame from any yielding function unless it is a super short time (like the 2ms example here—which is fairly unlikely to be honest). So either will do even if await-interaction-response is technically better for that first INP.
The await-interaction-response function is also less useful as a general purpose utility function as the correct answer here is not to split that 500ms job into 2 but into multiple parts (of 50ms or less so 10 or more parts ideally in this case!):
- Critical updates to allow users to see their interaction has been acknowledged (2ms) in this example.
- yield to allow a frame to draw
- Next part of task, ideally < 50ms
- yield to allow a frame to draw if needed
- Next part of task, ideally < 50ms
- yield to allow a frame to draw if needed
- ....etc.
This allows any future interactions to also be handled well as without splitting up that 498ms task it's just a trap lurking in your code that anyone could fall into.
But if those tasks are all quite small (e.g. 2ms) there using await-interaction-response would be a lot of wastage as it wouldn't do anything until the next frame:
Compare this with just yielding with yieldtomain and letting the browser decide how best to schedule the tasks and frames:
As you can see, more of the work gets done quicker in the second scenario.
Even in the scenario of the next task being 50ms if it was properly split up, then using yieldToMain wouldn't be too bad because it would:
- Do the 2ms initial task, yield
- Maybe decide to take on another task (50ms).
- Then render a frame.
Yes it would have overshot the 16ms frame but would still be well under 200ms INP target.
However if your next task was a lot longer than 50ms then there definitely is a benefit to using await-interaction-response over yieldToMain.
So maybe you do want to guarantee a good INP as much as possible, in which case it might be good to use a combination of the two and use after await-interaction-response the first updates, and then for the yieldToMain next ones:
Here you're accepting there might be a delay in the first frame, but think that's better than risking it so you can get that crucial first update out. That's a pretty good option.
If all this sounds a bit complicated then you're not wrong! So in general, I'd advocate for splitting the tasks liberally using something like yieldToMain at good yield points and then letting the browser worry about what to schedule when. In most cases that should strike a good balance between optimizing INP and also getting the work done, without having to think too much about it.