Skip to content
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

Merged
merged 9 commits into from
Oct 16, 2024

Conversation

westonruter
Copy link
Member

Fixes #64483.

What?

Use scheduler.yield() in the splitTask() function in @wordpress/interactivity when available. Otherwise, continue to fall back to using setTimeout 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 using setTimeout() as shown in the Optimize long tasks article on web.dev. That article also describes a more performant mechanism in scheduler.yield() which would be coming at a later date. That date is now in Chromium browsers (caniuse).

In #62665 the yieldToMain() function was renamed to splitTask() 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):

  1. Scripting time is reduced by 21%.
  2. Painting time is reduced by 36%.
  3. Idle time is reduced by 57% as is seen in the elimination of gaps in the flame chart.
  4. Hydration completes before the load event.

Splitting tasks with setTimeout()

See trace.cafe.

Screenshot 2024-10-09 16 49 03

image

Splitting tasks with scheduler.yield()

See trace.cafe

Screenshot 2024-10-09 16 49 09

image

Additional Observation

When scheduler.yield() is used, I see a 99ms long task caused by flushAfterPaintEffects:

image

When setTimeout() is used, these tasks are split up (where the black outlines indicate instances of flushAfterPaintEffects):

image

This appears to be due to the fact that flushAfterPaintEffects itself is scheduled for execution using a timer, meaning that when scheduler.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 introducing scheduler.yield() there as well (cf. facebook/react#27069).

@westonruter westonruter added [Type] Performance Related to performance efforts [Feature] Interactivity API API to add frontend interactivity to blocks. [Packages] Interactivity /packages/interactivity labels Oct 10, 2024
Copy link

github-actions bot commented Oct 10, 2024

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 props-bot label.

Unlinked Accounts

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

Unlinked contributors: brendankenny, LeszekSwirski.

Co-authored-by: westonruter <[email protected]>
Co-authored-by: swissspidy <[email protected]>
Co-authored-by: felixarntz <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Comment on lines 52 to 65
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 );
}
} );
Copy link
Member

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 );
	} );
}

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've applied that in 70077b8 and then further improved it in d5c51a6 by only doing the check for scheduler.yield() once.

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Humm. Actually this results in an error for some reason:

image

I guess it's because yield here doesn't have scheduler as its context object.

Copy link
Member Author

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().

Copy link
Member Author

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.

Copy link
Member

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.

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.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @westonruter!

@westonruter westonruter merged commit cd8d4dc into trunk Oct 16, 2024
65 checks passed
@westonruter westonruter deleted the add/scheduler-yield-split-task branch October 16, 2024 01:56
@github-actions github-actions bot added this to the Gutenberg 19.5 milestone Oct 16, 2024
};
export const splitTask =
typeof window.scheduler?.yield === 'function'
? window.scheduler.yield.bind( window.scheduler )
Copy link
Contributor

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.

Copy link
Member Author

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.

@t-hamano
Copy link
Contributor

t-hamano commented Nov 2, 2024

This PR may have had an impact on the lightbox. Please see #66691.

karthick-murugan pushed a commit to karthick-murugan/gutenberg that referenced this pull request Nov 13, 2024
* Leverage scheduler.yield in splitTask when available

* Eliminate extra Promise when using scheduler.yield()

Co-authored-by: swissspidy <[email protected]>

* Avoid needlessly re-checking whether scheduler.yield() is defined

* Remove needless function wrapper

* Ensure that scheduler is context object to yield function

* Remove needless async

* Reduce verbosity of scheduler.yield() existence check

* Fix return type for scheduler.yield()

Co-authored-by: Brendan Kenny <[email protected]>

* Define scheduler and scheduler.yield as optional

Co-authored-by: Brendan Kenny <[email protected]>

---------


Unlinked contributors: brendankenny, LeszekSwirski.
Co-authored-by: felixarntz <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Interactivity API API to add frontend interactivity to blocks. [Packages] Interactivity /packages/interactivity [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Leverage scheduler.yield() in @wordpress/interactivity'ss splitTask() when available
7 participants