-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Leverage scheduler.yield in splitTask when available #66001
Conversation
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @brendankenny, @LeszekSwirski. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
packages/interactivity/src/utils.ts
Outdated
return new Promise( async ( resolve ) => { | ||
if ( | ||
'scheduler' in window && | ||
typeof window.scheduler === 'object' && | ||
null !== window.scheduler && | ||
'yield' in window.scheduler && | ||
typeof window.scheduler.yield === 'function' | ||
) { | ||
await window.scheduler.yield(); | ||
resolve( undefined ); | ||
} else { | ||
setTimeout( resolve, 0 ); | ||
} | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more readable IMO and avoids an async function in the promise body:
export const splitTask = () => {
if (
'scheduler' in window &&
typeof window.scheduler === 'object' &&
null !== window.scheduler &&
'yield' in window.scheduler &&
typeof window.scheduler.yield === 'function'
) {
return window.scheduler.yield();
}
return new Promise( ( resolve ) => {
setTimeout( resolve, 0 );
} );
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, perfect. I spaced on this. I got the same feedback from @LeszekSwirski.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And improved yet further in 2516cd4 by removing the wrapper function around scheduler.yield()
. So when scheduler.yield()
is available, splitTask
is just a reference to that function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in a0933f4 via bind()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's probably zero difference in the end between:
window.scheduler.yield.bind( window.scheduler )
And:
() => {
return window.scheduler.yield();
}
Since in both cases a new function is involved. The former seems to be syntactic sugar for the latter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But why use bind
over the latter? I find the latter more intuitive to understand. Also, I'm not familiar enough with bind
to be confident so that is the same. Since you also say "probably", I'd suggest we go with the latter.
We have to use a function anyway for the fallback of returning a promise, so we might as well wrap the entire code in a function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"bind" is very very slightly preferable for the JS engine, since it can represent the result as a "bound function" that forwards directly to the function it binds already while resolving the call, and doesn't need to parse it, compile it, interpret bytecode, etc.
Co-authored-by: Brendan Kenny <[email protected]>
Co-authored-by: Brendan Kenny <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @westonruter!
}; | ||
export const splitTask = | ||
typeof window.scheduler?.yield === 'function' | ||
? window.scheduler.yield.bind( window.scheduler ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other places in Gutenberg, I believe we use requestIdleCallback
to optimize for UI responsiveness and delay some tasks with lower priority. I wonder if you can speak about the difference and when to favor one over the other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With requestIdleCallback
the idleness may occur several seconds after requested which would not be desirable for hydration. For splitTask
it's purely to break up long tasks which remain high priority. So both have their use cases.
This PR may have had an impact on the lightbox. Please see #66691. |
Fixes #64483.
What?
Use
scheduler.yield()
in thesplitTask()
function in@wordpress/interactivity
when available. Otherwise, continue to fall back to usingsetTimeout
as currently.Why?
In #58227 a
yieldToMain()
function was introduced to break up long hydration tasks when a document is initialized by the Interactivity API. At the time, this function was implemented usingsetTimeout()
as shown in the Optimize long tasks article on web.dev. That article also describes a more performant mechanism inscheduler.yield()
which would be coming at a later date. That date is now in Chromium browsers (caniuse).In #62665 the
yieldToMain()
function was renamed tosplitTask()
and was then exported by the@wordpress/interactivity
package for extensions to leverage (e.g. in Async Actions).On a post that contains 38 interactive blocks (36 of which are Image blocks with lightboxes) the performance benefits of using
scheduler.yield()
in the following two traces (both run with 6x CPU slowdown on a HP Dragonfly Elite Chromebook):load
event.Splitting tasks with
setTimeout()
See trace.cafe.
Splitting tasks with
scheduler.yield()
See trace.cafe
Additional Observation
When
scheduler.yield()
is used, I see a 99ms long task caused byflushAfterPaintEffects
:When
setTimeout()
is used, these tasks are split up (where the black outlines indicate instances offlushAfterPaintEffects
):This appears to be due to the fact that
flushAfterPaintEffects
itself is scheduled for execution using a timer, meaning that whenscheduler.yield()
is used, the pending paint effects are allowed to bunch up for one single call. This is part of Preact, and perhaps that long task can be addressed upstream by introducingscheduler.yield()
there as well (cf. facebook/react#27069).