Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback - #81249
Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback#81249nickchomey wants to merge 4 commits into
Conversation
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
|
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 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. |
e786b6c to
531e984
Compare
Closes #58225
Relates to: PR #58284 — "Interactivity API: Defer hydration until node is scrolled near the viewport" (@westonruter ). This is an updated, rebased-on-trunk version of that work, incorporating the maintainer discussion (notably @luisherranz's request for an idle-CPU fallback) and fixes for regressions found along the way.
I was not able to edit #58284 directly, so just ported those changes to
trunkand added idle hydration and tests. I dont know how to, so feel free to add attribution to whoever should have it (I see @westonruter, @felixarntz, @luisherranz. @sirreal). I removed @sirreal's scroll-to-bottom edits to the wp-each directive test, as they were no longer necessary after my improvements.The problem
Today,
@wordpress/interactivityhydrates every interactive island atDOMContentLoaded. On pages with many blocks below the fold, that means running hydration JS (and firingdata-wp-init/data-wp-watchcallbacks) for content the user may never scroll to — contributing to slow initial load on long pages (feeds, archives, long-form posts).What this does
Defers hydration of each interactive island until it is near the viewport, and guarantees the rest are hydrated when the CPU is idle:
IntersectionObserver(rootMargin: one viewport height, top and bottom) hydrates each island as it approaches the viewport.requestIdleCallback, bounded by atimeout: 2000) hydrates everything remaining within a bounded time, guaranteeing that islands the user never scrolls to — and the router regions the router needs to diff — are hydrated even on a busy page. This also keeps the router'sinitialVdomPromisecaching reliable (the original hang that plagued early iterations of Interactivity API: Defer hydration until node is scrolled near the viewport #58284 is gone). Implemented via therequestidlecallbacknpm package, already used by@wordpress/priority-queue, which provides a comprehensive polyfill for Safari.Once hydration has completed (idle sweep ran, or the observer finished all islands),
hydrateAllRemaining()is a permanent no-op:renderElement()/renderHTML()PR Interactivity API: Add renderElement() and renderHTML() to hydrate HTML inserted into the live DOM #81190).Setof observed nodes (idempotentdelete) instead of a counter. We hit a flake during integration where duplicate intersection entries double-decremented the counter and prematurely disarmed the sweep; theSetmakes it idempotent.initialVdomPromiserather than re-runningtoVdomon the live DOM at module-eval time. Under lazy hydration, islands may not be hydrated yet when the router initializes, sotoVdomcould mark them as hydrated before hydration actually ran — a regression we hit when integrating the idle sweep with the router (the static-import race-condition failures).Testing
lazy-hydratione2e spec: 5 tests, all passing, run 10+ times with zero flakes.Screenshots
Here's a Performance Profile for a site with 978
data-wp-attributes in the document. 6x Slowdown. Filtered/highlighted forinteractivity.Before:

After:

The bulk moved to after the Load event. Interestingly, but perhaps unsurprisingly, the processing looks a lot more fragmented and takes longer.
I commented-out splitTask and the bars get much tighter. All test pass like this - is splitTask truly needed?
Decisions for reviewers
timeout: 2000— bounds how long we wait for a quiet CPU. On an idle page the sweep fires at the first idle moment (typically well under 2s); the timeout only caps the worst case (busy main thread). Do you want to increase the limit, or get rid of it altogether (waiting until whenever the main thread frees up, if ever)?As it turns out, there's currently no such mechanism in the iAPI for doing this. As such, PR Interactivity API: Add renderElement() and renderHTML() to hydrate HTML inserted into the live DOM #81190 could be considered a companion to this PR, as it brings precisely these capabilities via two functions:
renderElement()/renderHTML(). Of course anything there is open to discussion and change.requestidlecallbackpolyfill — reused from@wordpress/priority-queuerather than a new polyfill; no-op on modern browsers.splitTask()calls that wraprender()necessary? It seems to perform much better without them and all tests pass.Use of AI Tools
Deepseek V4 Flash via vscode