Skip to content

Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback - #81249

Open
nickchomey wants to merge 4 commits into
WordPress:trunkfrom
nickchomey:try/iapi-lazy-hydration
Open

Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback#81249
nickchomey wants to merge 4 commits into
WordPress:trunkfrom
nickchomey:try/iapi-lazy-hydration

Conversation

@nickchomey

@nickchomey nickchomey commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 trunk and 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/interactivity hydrates every interactive island at DOMContentLoaded. On pages with many blocks below the fold, that means running hydration JS (and firing data-wp-init / data-wp-watch callbacks) 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:

  • An IntersectionObserver (rootMargin: one viewport height, top and bottom) hydrates each island as it approaches the viewport.
  • A one-shot idle-time sweep (requestIdleCallback, bounded by a timeout: 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's initialVdomPromise caching 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 the requestidlecallback npm package, already used by @wordpress/priority-queue, which provides a comprehensive polyfill for Safari.
  • Router integration: the router force-hydrates all remaining islands before rendering a navigation, so its regions are always available for diffing regardless of scroll position or idle timing. Router-rendered regions are preact-rendered (fully interactive) and are never re-hydrated.

Once hydration has completed (idle sweep ran, or the observer finished all islands), hydrateAllRemaining() is a permanent no-op:

  • Islands inserted into the DOM later (e.g. raw HTML) are not auto-hydrated — developers hydrate injected markup explicitly (see the companion renderElement() / renderHTML() PR Interactivity API: Add renderElement() and renderHTML() to hydrate HTML inserted into the live DOM #81190).
  • The observer's "all done" detection uses a Set of observed nodes (idempotent delete) instead of a counter. We hit a flake during integration where duplicate intersection entries double-decremented the counter and prematurely disarmed the sweep; the Set makes it idempotent.
  • The router caches the initial page from initialVdomPromise rather than re-running toVdom on the live DOM at module-eval time. Under lazy hydration, islands may not be hydrated yet when the router initializes, so toVdom could 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

  • New lazy-hydration e2e spec: 5 tests, all passing, run 10+ times with zero flakes.
  • Full interactivity e2e suite: 234/234 passing.
  • Unit tests unaffected (this is e2e-tested behavior; the observer/sweep are browser-timing features).

Screenshots

Here's a Performance Profile for a site with 978 data-wp- attributes in the document. 6x Slowdown. Filtered/highlighted for interactivity.

Before:
Screenshot_2026-08-05_19-48-47

After:
Screenshot_2026-08-05_19-48-37

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?

Screenshot_2026-08-05_20-05-43

Decisions for reviewers

  1. 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)?
  2. 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: 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.
  3. requestidlecallback polyfill — reused from @wordpress/priority-queue rather than a new polyfill; no-op on modern browsers.
  4. Are the two splitTask() calls that wrap render() necessary? It seems to perform much better without them and all tests pass.

Use of AI Tools

Deepseek V4 Flash via vscode

@github-actions github-actions Bot added [Package] Interactivity /packages/interactivity [Package] Interactivity Router /packages/interactivity-router labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

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.

  • Required label: Any label starting with [Type].
  • Labels found: [Package] Interactivity, [Package] Interactivity Router.

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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: nickchomey <nickchomey@git.wordpress.org>
Co-authored-by: luisherranz <luisherranz@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>

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

@nickchomey nickchomey changed the title Try/iapi lazy hydration Interactivity API: Defer hydration of interactive islands until they approach the viewport, with an idle-time fallback Aug 5, 2026
@nickchomey nickchomey changed the title Interactivity API: Defer hydration of interactive islands until they approach the viewport, with an idle-time fallback Interactivity API: Lazy hydration of interactive islands, with an idle-time fallback Aug 5, 2026
@nickchomey
nickchomey force-pushed the try/iapi-lazy-hydration branch from e786b6c to 531e984 Compare August 6, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Interactivity Router /packages/interactivity-router [Package] Interactivity /packages/interactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interactivity API: Hydration causes long task

1 participant