Skip to content

Preserve hook state when Suspense re-suspends - #5239

Merged
JoviDeCroock merged 2 commits into
mainfrom
fix/suspense-preserve-mounted-hooks
Sep 4, 2026
Merged

Preserve hook state when Suspense re-suspends#5239
JoviDeCroock merged 2 commits into
mainfrom
fix/suspense-preserve-mounted-hooks

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Member

Preserves committed hook state when a Suspense boundary re-suspends while still resetting hooks for an initial suspended mount. Effects are cleaned up and re-run when content is revealed again, with client-rendering and hydration coverage.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: unsure 🔍 -0% - +1% (-2.20ms - +8.44ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +1% (-0.02ms - +0.09ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -3% - +2% (-1.67ms - +0.95ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -0% - +0% (-0.07ms - +0.06ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -2% - +2% (-1.56ms - +1.70ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -5% - +5% (-0.07ms - +0.07ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -1% - +3% (-0.10ms - +0.51ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -4% - +5% (-0.96ms - +1.32ms)
    preact-local vs preact-main

usedJSHeapSize

  • create10k: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -6% - +2% (-0.33ms - +0.13ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -0% - +0% (-0.01ms - +0.01ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -5% - +4% (-0.06ms - +0.04ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -0% - +0% (-0.01ms - +0.01ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local682.86ms - 690.62ms-unsure 🔍
-0% - +1%
-2.20ms - +8.44ms
preact-main679.98ms - 687.26msunsure 🔍
-1% - +0%
-8.44ms - +2.20ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local19.09ms - 19.09ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main19.09ms - 19.09msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-
filter-list

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.51ms - 16.60ms-unsure 🔍
-0% - +1%
-0.02ms - +0.09ms
preact-main16.48ms - 16.56msunsure 🔍
-1% - +0%
-0.09ms - +0.02ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.55ms - 1.56ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main1.56ms - 1.56msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-
hydrate1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local54.87ms - 57.10ms-unsure 🔍
-3% - +2%
-1.67ms - +0.95ms
preact-main55.65ms - 57.04msunsure 🔍
-2% - +3%
-0.95ms - +1.67ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local5.05ms - 5.29ms-unsure 🔍
-6% - +2%
-0.33ms - +0.13ms
preact-main5.07ms - 5.46msunsure 🔍
-3% - +6%
-0.13ms - +0.33ms
-
many-updates

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.46ms - 16.59ms-unsure 🔍
-0% - +0%
-0.07ms - +0.06ms
preact-main16.52ms - 16.55msunsure 🔍
-0% - +0%
-0.06ms - +0.07ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.74ms - 3.74ms-unsure 🔍
-0% - +0%
-0.01ms - +0.01ms
preact-main3.74ms - 3.75msunsure 🔍
-0% - +0%
-0.01ms - +0.01ms
-
replace1k
  • Browser: chrome-headless
  • Sample size: 100
  • Built by: CI #5914
  • Commit: aa0cf80

duration

VersionAvg timevs preact-localvs preact-main
preact-local84.78ms - 87.02ms-unsure 🔍
-2% - +2%
-1.56ms - +1.70ms
preact-main84.65ms - 87.02msunsure 🔍
-2% - +2%
-1.70ms - +1.56ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.01ms - 3.01ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main3.01ms - 3.01msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local27.82ms - 29.94ms-unsure 🔍
-2% - +6%
-0.70ms - +1.60ms
preact-main27.99ms - 28.86msunsure 🔍
-5% - +2%
-1.60ms - +0.70ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local33.50ms - 35.00ms-unsure 🔍
-2% - +4%
-0.58ms - +1.43ms
preact-main33.15ms - 34.49msunsure 🔍
-4% - +2%
-1.43ms - +0.58ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local32.79ms - 34.42ms-unsure 🔍
-5% - +2%
-1.55ms - +0.74ms
preact-main33.20ms - 34.82msunsure 🔍
-2% - +5%
-0.74ms - +1.55ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local29.52ms - 30.34ms-unsure 🔍
-2% - +2%
-0.72ms - +0.57ms
preact-main29.51ms - 30.50msunsure 🔍
-2% - +2%
-0.57ms - +0.72ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local20.49ms - 20.74ms-unsure 🔍
-1% - +1%
-0.19ms - +0.15ms
preact-main20.51ms - 20.75msunsure 🔍
-1% - +1%
-0.15ms - +0.19ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local27.52ms - 28.12ms-unsure 🔍
-2% - +1%
-0.58ms - +0.38ms
preact-main27.54ms - 28.30msunsure 🔍
-1% - +2%
-0.38ms - +0.58ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 240
  • Built by: CI #5914
  • Commit: aa0cf80

duration

VersionAvg timevs preact-localvs preact-main
preact-local1.36ms - 1.47ms-unsure 🔍
-5% - +5%
-0.07ms - +0.07ms
preact-main1.37ms - 1.47msunsure 🔍
-5% - +5%
-0.07ms - +0.07ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.07ms - 1.14ms-unsure 🔍
-5% - +4%
-0.06ms - +0.04ms
preact-main1.08ms - 1.15msunsure 🔍
-4% - +5%
-0.04ms - +0.06ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local15.49ms - 16.01ms-unsure 🔍
-1% - +3%
-0.10ms - +0.51ms
preact-main15.39ms - 15.70msunsure 🔍
-3% - +1%
-0.51ms - +0.10ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.27ms - 1.27ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main1.27ms - 1.27msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-
update10th1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local26.55ms - 28.05ms-unsure 🔍
-4% - +5%
-0.96ms - +1.32ms
preact-main26.27ms - 27.99msunsure 🔍
-5% - +4%
-1.32ms - +0.96ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.96ms - 2.97ms-unsure 🔍
-0% - +0%
-0.01ms - +0.01ms
preact-main2.96ms - 2.97msunsure 🔍
-0% - +0%
-0.01ms - +0.01ms
-

tachometer-reporter-action v2 for CI

@coveralls

coveralls commented Sep 4, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 99.856%. remained the same — fix/suspense-preserve-mounted-hooks into main

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Size Change: +45 B (+0.28%)

Total Size: 15.9 kB

📦 View Changed
Filename Size Change
compat/dist/compat.mjs 3.92 kB +41 B (+1.06%)
hooks/dist/hooks.mjs 1.58 kB +4 B (+0.25%)
ℹ️ View Unchanged
Filename Size
debug/dist/debug.mjs 3.88 kB
devtools/dist/devtools.mjs 273 B
dist/preact.mjs 4.89 kB
jsx-runtime/dist/jsxRuntime.mjs 868 B
test-utils/dist/testUtils.mjs 473 B

compressed-size-action

@JoviDeCroock
JoviDeCroock marked this pull request as ready for review September 4, 2026 10:12
JoviDeCroock added a commit that referenced this pull request Sep 4, 2026
Alternative to the approach in #5239: instead of tracking whether the
Suspense boundary has committed via lifecycle hooks and threading a
preserveHooks flag through detachedClone, discard hooks per component in
options._catchError when the suspending vnode never committed
(oldVNode has no component). detachedClone then always keeps hook state,
runs effect cleanups, and clears effect args so effects re-run on reveal.

Also drops effects queued by the aborted render (_pendingEffects and
_renderCallbacks), which otherwise made a changed-deps layout effect run
twice on reveal.

Size: compat +36 B br, hooks +6 B br (vs +70 B / +6 B for #5239).
@JoviDeCroock
JoviDeCroock force-pushed the fix/suspense-preserve-mounted-hooks branch from 3ece2a9 to bb593d1 Compare September 4, 2026 10:13
Alternative to the approach in #5239: instead of tracking whether the
Suspense boundary has committed via lifecycle hooks and threading a
preserveHooks flag through detachedClone, discard hooks per component in
options._catchError when the suspending vnode never committed
(oldVNode has no component). detachedClone then always keeps hook state,
runs effect cleanups, and clears effect args so effects re-run on reveal.

Also drops effects queued by the aborted render (_pendingEffects and
_renderCallbacks), which otherwise made a changed-deps layout effect run
twice on reveal.

Size: compat +36 B br, hooks +6 B br (vs +70 B / +6 B for #5239).
@JoviDeCroock
JoviDeCroock force-pushed the fix/suspense-preserve-mounted-hooks branch from bb593d1 to aa0cf80 Compare September 4, 2026 10:29
Comment thread mangle.json
"$_hydrationMismatch": "__m",
"$_list": "__",
"$_pendingEffects": "__h",
"$_passive": "__P",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Q: Does this clash with _parentDom?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

_passive is set on hooks and _parentDom is set on the vnode/component so it should not clash

@JoviDeCroock
JoviDeCroock merged commit e9415cf into main Sep 4, 2026
13 checks passed
@JoviDeCroock
JoviDeCroock deleted the fix/suspense-preserve-mounted-hooks branch September 4, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants