yieldToMain vs await-interaction-response

174 views
Skip to first unread message

Matthew DeLambo

unread,
Jun 14, 2024, 11:01:15 AMJun 14
to web-vitals-feedback
We have been using `yieldToMain` (described here) to break up some of our large React processes with pretty good success.

Vercel recently promoted a similar function, `await-interaction-response` (described here) to accomplish basically the same thing. Is there a difference between the two, or is one more optimal?

Barry Pollard

unread,
Jun 14, 2024, 11:50:37 AMJun 14
to Matthew DeLambo, web-vitals-feedback
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!):
  1. Critical updates to allow users to see their interaction has been acknowledged (2ms) in this example.
  2. yield to allow a frame to draw
  3. Next part of task, ideally < 50ms
  4. yield to allow a frame to draw if needed
  5. Next part of task, ideally < 50ms
  6. yield to allow a frame to draw if needed
  7. ....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:

image.png
Compare this with just yielding with yieldtomain and letting the browser decide how best to schedule the tasks and frames:

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

image.png

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.
In fact we're considering adding a scheduler.render native function to allow this, and until then await-interaction-response is a pretty good polyfill for this.

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.

Thanks,
Barry

On Fri, 14 Jun 2024 at 16:01, 'Matthew DeLambo' via web-vitals-feedback <web-vital...@googlegroups.com> wrote:
We have been using `yieldToMain` (described here) to break up some of our large React processes with pretty good success.

Vercel recently promoted a similar function, `await-interaction-response` (described here) to accomplish basically the same thing. Is there a difference between the two, or is one more optimal?

--
You received this message because you are subscribed to the Google Groups "web-vitals-feedback" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web-vitals-feed...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/web-vitals-feedback/3c0a02df-b285-4024-a57f-12957da7d364n%40googlegroups.com.

Michal Mocny

unread,
Jun 14, 2024, 11:52:34 AMJun 14
to Matthew DeLambo, web-vitals-feedback
The Optimizing-INP page actually describes a similar approach which is better than just yieldToMain.

"A more advanced yielding technique involves structuring the code in your event callbacks to limit what gets run to just the logic required to apply visual updates for the next frame. Everything else can be deferred to a subsequent task"

"While the use of setTimeout() inside a requestAnimationFrame() call in the previous code example is admittedly a bit esoteric, it is an effective method that works in all browsers to ensure that the non-critical code does not block the next frame."

Looks like we didn't update the optimize-long-tasks doc to add similar advise there.

The new vercel announced uses this pattern (rAF() first to schedule at the start of rendering, the setTimeout(0) second to "yieldToMain").  In other words: yieldToMainAfterRenderingStarts, or as I typically name the function "afterNextPaint".

Vercel team wisely also added a setTimeout(100) as a fallback.  They did this because if the page visibility changes, or if rendering happens to be paused for other reasons, rAF() callback might not get invoked for a long time.  Vercel team prefers to schedule this work even if the page finds itself in that situation, rather than delay until foregrounded again.  (Browsers might throttle JS anyway on some platforms, but I think it's probably wise to do as they suggest).

---

One addendum to that vercel blog post from myself: all of the above approaches are 100% vanilla-js and not react-specific.  They are a great tool to help with INP.

However, if you know you have a React site, you might be able to leverage more integrated tools-- such as transitions, useDeferredValue, async actions, etc.  That way, rather than delaying the application of a state update, which might also delay the start of rendering or the start of network requests, you can leverage the fact that React already supports "time slicing" background renders.

Cheers

On Fri, Jun 14, 2024 at 11:01 AM 'Matthew DeLambo' via web-vitals-feedback <web-vital...@googlegroups.com> wrote:
We have been using `yieldToMain` (described here) to break up some of our large React processes with pretty good success.

Vercel recently promoted a similar function, `await-interaction-response` (described here) to accomplish basically the same thing. Is there a difference between the two, or is one more optimal?

--

Matthew DeLambo

unread,
Jun 18, 2024, 7:06:58 AMJun 18
to web-vitals-feedback
Great responses, thanks all!
Reply all
Reply to author
Forward
0 new messages