Repository navigation
Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback - #81249
nickchomey wants to merge 13 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. |
e786b6c to
531e984
Compare
531e984 to
e51da7a
Compare
…ches don't block. Was accidentally moved during porting
|
One other thought: it seems to me that the various mechanisms are sort of running at cross-purposes here. Some ideas:
The question raised in the OP was what timeout is appropriate (2000ms was selected arbitrarily)? On one hand, no/infinite timeout is ideal so as to not hydrate things that are not needed. On the other hand, i think the idea behind What if we just move
I'll try to explore these ideas and provide some traces when I have a chance. |
|
Just stumbled on this comment while looking into other preact stuff... preactjs/preact#409 (comment) import { options } from 'preact';
options.debounceRendering = requestIdleCallbackThey rejected a PR to implement requestIdleCallback within preact (v8), and said that it was now possible in userland in v10 with this option/hook. I see that the same option was mentioned here #70874 (comment) I'll try to test it out soon. |
|
Another idea to try out: lazy hydrate within an island. If you have, for example, a feed of posts, the island's start will be within the initial viewport, but the whole island (and all of its posts) wont be. Would be great if possible to defer hydration of posts until they come close to being in the viewport (and, as proposed in previous comments, not lazy load them at all until a client-side navigation forces it). This would be directly useful for my own application a couple good articles: https://philipwalton.com/articles/idle-until-urgent/ |
|
Another thought that just occurred to me... Without having dug into the code again, i think we loop the top-level regions/islands and hydrate them. But, since islands are distinct, i dont see why we couldn't hydrate all the regions in parallel with something like Promise.all... Something to try and test when i pick this back up |
🤖 PR meta 🤖🎉 PropsUnlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @awss1i. 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. Updated as activity occurs, without notifying anyone named here. Add the 🏷️ LabelsThis pull request needs exactly one label indicating its type, and has 0.
Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you. |
|
Oh god. somehow merging trunk added 1800 commits, lots of tags, and notified a lot of maintainers. Apologies. I will fix. edit: if anyone is curious, i think it is a peculiarity of jj vcs (or probably just underlying git, or github...). if you do |
Haha thats odd, no worries. |
Fold trunk a51f2e3 into the lazy-hydration branch, keeping PR behavior on top of trunk refactors: - packages/interactivity: keep trunk vitest devDependency alongside PR requestidlecallback types; opt into requestidlecallback types in tsconfig.build.json. - hydration.ts: keep trunk top-level island filter and JSDoc; keep PR observer, idle sweep, hydrateAllRemaining, and the no-Observer fallback. - vdom.ts: restore hydratedIslands WeakSet trunk removed; the PR double-hydrate guard needs it. - router index.ts: keep trunk lifecycle-token rewrite; splice the PR hydrateAllRemaining calls into popstate and navigate.
d4a3b50 to
e5b11bc
Compare
59d0051 to
8565be6
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)? I'm actually preferential to not forcing the hydration at all - hydrating elements/directives that are out of the viewport is just wasted battery life on mobile phones. The hydration will happen if they scroll and will ultimately be forced before a router navigation.requestidlecallbackpolyfill — reused from@wordpress/priority-queuerather than a new polyfill; no-op on modern browsers. Similar to the previous point, this adds over 1KB min.gz to the bundle, which is a lot for something that we might not even need...One-shot semantics — Concerns were raised in in Interactivity API: Defer hydration until node is scrolled near the viewport #58284 about how to handle html that might have been
added outside of the iAPI. @luisherranz decided "I don’t think we should consider that scenario. If someone wants to modify the DOM and have the Interactivity API recognize those changes, they should do it through the methods provided by the Interactivity API."
As it turns out, there's currently no such mechanism in the iAPI for doing this. As such, PR Interactivity API: renderHTML function to insert and hydrate html strings #81462 could be considered a companion to this PR, as it brings precisely these capabilities via its
renderHTML()function. Of course anything there is open to discussion and change.Are the two
splitTask()calls that wraptoVdom()necessary? It seems to perform much better without them, and all tests pass.Use of AI Tools
Deepseek V4 Flash via vscode