Yielding to main thread

683 views
Skip to first unread message

Adam Cable

unread,
Aug 30, 2023, 11:55:38 AM8/30/23
to web-vitals-feedback
Hi,

I've been through a ton of reading material and videos about improving INP, and come to the conclusion that the easiest way to achieve this for projects that utilise javascript code is to wrap the following around chunks of code to break it up into separate pieces of work:

window.requestAnimationFrame(function() {setTimeout((function(){

CODE HERE

}));});

This seems to let the browser paint whatever it needs to inbetween larger chunks of code. Is there a better/easier way, or is this currently the "best practice" way?

Thanks,
Adam

Alex Loaiza

unread,
Aug 30, 2023, 12:45:13 PM8/30/23
to web-vitals-feedback
Interesting method! 

Michal Mocny

unread,
Aug 30, 2023, 12:47:54 PM8/30/23
to Adam Cable, web-vitals-feedback
That is a great pattern to have in your back pocket!  Here is an article about it.

I typically use it as such, personally:

async function afterNextPaint() {
  return new Promise(resolve = requestAnimationFrame(() => setTimeout(resolve, 0));
}

// Later
await afterNextPaint();

However, although this explicitly delays running code until after the browser has a chance to run next paint-- something to realize is that you usually want to be careful to not actually delay critical feedback after interactions.

For example, in my event handlers, I try to be careful to provide initial feedback, in the form on a light touch DOM update, css styles etc, then await afterNextPaint, and then followup with anything compute or rendering intensive.  You may also want to periodically yield() even beyond that first hop.


--
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/d964ea2a-97a9-4e1b-99c3-faec61a567edn%40googlegroups.com.

Adam Cable

unread,
Aug 30, 2023, 1:16:57 PM8/30/23
to web-vitals-feedback
Magic, thanks for all the great info and (once again) such a timely response :)

Adam

Abhishek Chaudhary

unread,
Aug 31, 2023, 12:00:12 PM8/31/23
to web-vitals-feedback
Hi,
I am using setTimeout( function(){   }, 0 ) for the click events to defer my non-critical code, and it's working fine for me. But now I want to know which of the below three methods works the best:

1) setTimeout(function(){
    // code
    } , 0);

2) window.requestAnimationFrame(function() {setTimeout((function(){
     //CODE HERE
     }));});

3) async function afterNextPaint() {
    return new Promise(resolve = requestAnimationFrame(() => setTimeout(resolve, 0));
    }

    // Later 
    await afterNextPaint();

Thanks
Abhishek

Michal Mocny

unread,
Aug 31, 2023, 10:01:37 PM8/31/23
to Abhishek Chaudhary, web-vitals-feedback
The bottom two options you listed are equivalent, just written in different styles.

They both explicitly wait for next animation frame, then use setTimeout(..., 0) to yield one more time, to guarantee that rendering is actually complete before running.

Your first option only uses a timeout and doesn't wait for rendering.  In the context of INP and specifically deferring work until after next paint, the last two options are better.

I would only use the first option in general cases of "yielding" to split up long tasks, but which aren't specifically waiting for next paint.

Abhishek Chaudhary

unread,
Sep 1, 2023, 12:21:26 AM9/1/23
to Michal Mocny, web-vitals-feedback
Thanks, Michal !!

Abhishek Chaudhary

unread,
Sep 4, 2023, 6:45:19 AM9/4/23
to Michal Mocny, web-vitals-feedback
Hi Michal,
I had one more doubt.
 
Case 1:

onClick={ () => {
     criticalTask();      // a UI update, takes 200 ms for execution
     
     requestAnimationFrame( () => {
        setTimeout( () => 
         nonCriticalTask();          // some GA trackings, takes 30 ms for execution (using setTimeout only would have                                                    also worked here )
         ,0);
    }
   }
}

INP in case 1
Input delay: 40 ms
Processing time: 200 ms
Presentation delay: 30 ms
Overall INP: 270 ms

Here overall time taken for the visual update would be close to 270 ms.

-----------------------------------------------------------------------------------------
Case 2:
onClick={ () => {
     nonCriticalTask();      // some GA trackings, takes 30 ms for execution

     requestAnimationFrame( () => {
        setTimeout( () => 
         criticalTask();   // a UI update, takes 200 ms for execution 
         ,0);
    }
   }
}

INP in case 2
Input delay: 40 ms
Processing time: 30 ms         // targeting processing time
Presentation delay: 30 ms
Overall INP: 100 ms

Here, the overall time taken for the visual update would be as:
 INP 
 + 
 criticalTask()     // 200 ms execution time 
 + 
 time taken to execute setTimeout in the event loop       // roughly 30 ms in my case 

Overall visual feedback time= 100 +200 +40 = 330 ms      // 270 ms in case 1

Here by implementing case 2 user is not experiencing any delay in the UI update as visual feedback got delayed by a factor of 50 - 60 ms only.

--------------------------------------------------------------------------
So I just want to know if this case 2 implementation is a good/ethical way to reduce INP ? 
Please share your valuable input.

Thanks 
Abhishek

Barry Pollard

unread,
Sep 4, 2023, 7:18:57 AM9/4/23
to Abhishek Chaudhary, Michal Mocny, web-vitals-feedback
Case 2 will help the INP of that interaction and IMHO prioritising the next frame—at the expense of the heavy processing—is a good tactic and what INP is trying to encourage. You can see Michal discussing this here: https://www.youtube.com/watch?v=KZ1kxzsJZ5g&t=382s

However, while delaying the main work will help this interaction, it could impact the INP of the next interaction as there is a 200ms price still to be paid in the next animation frame. This can also make it more difficult to understand and optimize as input delays are not as a result of that interaction, but other work and presentionation delays could be either. Processing delays are often easier to understand as you know the exact interaction and so code causing them.

You can see Michal talking about that here: https://www.youtube.com/watch?v=KZ1kxzsJZ5g&t=460s where he has delayed the heavy work, but still gets an occasional "hiccup", and later on can see this is due to this delay of previous work: https://www.youtube.com/watch?v=KZ1kxzsJZ5g&t=539s

So best practice of all is to split up the 200ms, rather than just delay, so it may be spread across multiple frames if needs be (i.e. so other interactions can also interrupt that 200ms rather than wait for it to complete).

Reply all
Reply to author
Forward
0 new messages