Skip to content

fix inconsistent effects for suspended nodes#3991

Merged
Madoshakalaka merged 3 commits intomasterfrom
fix/suspense-use-effect-dom-ordering
Apr 24, 2026
Merged

fix inconsistent effects for suspended nodes#3991
Madoshakalaka merged 3 commits intomasterfrom
fix/suspense-use-effect-dom-ordering

Conversation

@Madoshakalaka
Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka commented Feb 19, 2026

Description

Fixes #3780

This PR gives a single atomic transition for every <Suspense> boundary: no child effect inside it observes the DOM until the boundary is fully un-suspended and the DOM is live.

Opus 4.7's research on React source code:

React arrives at the same invariant by a different route. Every <Suspense> wraps its primary content in an Offscreen fiber carrying an OffscreenPassiveEffectsConnected visibility bit. While the boundary is hidden the bit is off and passive effects are skipped during commit; when it flips back on, recursivelyTraverseReconnectPassiveEffects walks the subtree and fires every deferred useEffect at once. React stores the "defer" state as a flag on the fiber and walks the tree on each commit; This PR now store it as a BTreeMap<comp_id, PendingRendered> Vec<(usize, PendingRendered)> on BaseSuspense and drain it in one pass when the boundary un-suspends. Same guarantee, smaller footprint given Yew doesn't have an Offscreen abstraction.

BTreeMap<comp_id, PendingRendered> was first considered but given up on because it bloats each example roughly by 5kB (~5% increase) probablly because of monomorphization.

Checklist

  • I have reviewed my own code
  • I have added tests

Added two regression tests. Both fail on master.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Visit the preview URL for this PR (updated for commit 4dcca09):

https://yew-rs-api--pr3991-fix-suspense-use-eff-vd5sx1yr.web.app

(expires Fri, 01 May 2026 06:22:32 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.778 ns      │ 3.362 ns      │ 2.785 ns      │ 2.802 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.777 ns      │ 4.147 ns      │ 2.783 ns      │ 2.835 ns      │ 100     │ 1000000000

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 346.757 362.701 348.542 4.978
Hello World 10 487.910 501.476 491.678 4.616
Function Router 10 29651.437 30155.791 29912.198 190.776
Concurrent Task 10 1006.316 1008.140 1007.476 0.654
Many Providers 10 1024.518 1052.170 1037.899 10.530

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 346.809 347.272 347.007 0.138
Hello World 10 489.452 491.828 490.693 0.735
Function Router 10 30138.179 30766.335 30458.100 239.272
Concurrent Task 10 1006.530 1008.311 1007.750 0.557
Many Providers 10 1045.362 1116.267 1063.108 23.780

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
actix_ssr_router 612.039 612.796 +0.757 +0.124%
async_clock 100.003 100.492 +0.489 +0.489%
axum_ssr_router 612.034 612.791 +0.757 +0.124%
boids 163.725 164.209 +0.484 +0.296%
communication_child_to_parent 93.429 93.912 +0.483 +0.517%
communication_grandchild_with_grandparent 105.420 105.904 +0.484 +0.459%
communication_grandparent_to_grandchild 101.775 102.260 +0.484 +0.476%
communication_parent_to_child 90.858 91.342 +0.483 +0.532%
contexts 105.660 106.144 +0.483 +0.458%
counter 85.691 86.173 +0.481 +0.562%
counter_functional 87.711 88.192 +0.481 +0.549%
dyn_create_destroy_apps 89.579 90.061 +0.481 +0.537%
file_upload 99.204 99.686 +0.481 +0.485%
function_delayed_input 94.320 94.800 +0.479 +0.508%
function_memory_game 169.496 169.976 +0.479 +0.283%
function_router 399.064 399.552 +0.487 +0.122%
function_todomvc 164.153 164.635 +0.481 +0.293%
futures 234.658 235.145 +0.486 +0.207%
game_of_life 100.431 100.917 +0.486 +0.484%
immutable 258.316 258.965 +0.648 +0.251%
inner_html 80.535 81.017 +0.481 +0.598%
js_callback 109.245 110.032 +0.787 +0.720%
keyed_list 175.894 176.376 +0.482 +0.274%
mount_point 83.901 84.385 +0.483 +0.576%
nested_list 112.734 113.216 +0.481 +0.427%
node_refs 91.425 91.908 +0.483 +0.529%
password_strength 1717.396 1717.882 +0.486 +0.028%
portals 93.072 93.555 +0.482 +0.518%
router 365.799 366.286 +0.487 +0.133%
suspense 113.096 113.872 +0.776 +0.686%
timer 88.275 88.758 +0.482 +0.546%
timer_functional 98.731 99.216 +0.484 +0.491%
todomvc 141.285 141.768 +0.482 +0.341%
two_apps 85.894 86.375 +0.481 +0.561%
web_worker_fib 136.198 136.682 +0.483 +0.355%
web_worker_prime 184.590 185.073 +0.483 +0.262%
webgl 82.678 83.159 +0.481 +0.582%

✅ None of the examples has changed their size significantly.

@Madoshakalaka Madoshakalaka force-pushed the fix/suspense-use-effect-dom-ordering branch from 3bafe86 to c456a5f Compare February 19, 2026 06:06
@Madoshakalaka Madoshakalaka added the A-yew Area: The main yew crate label Feb 19, 2026
@Madoshakalaka Madoshakalaka marked this pull request as ready for review April 23, 2026 09:09
github-actions[bot]
github-actions Bot previously approved these changes Apr 23, 2026
github-actions[bot]
github-actions Bot previously approved these changes Apr 24, 2026
The previous approach routed a resuming child's rendered through the
scheduler's `rendered` queue, which drains only after the update+render
queues. That worked for a single suspender but not for siblings: if A
resumed while B was still pending, A's rendered still fired before the
boundary un-suspended, because Suspense's render with suspended=true
still doesn't shift children into the live tree.

Hand the pending rendered directly to the ancestor BaseSuspense as a
PendingRendered, keyed by comp_id in a Vec. BaseSuspense drains
this list in its own rendered lifecycle, gated on
self.suspensions.is_empty() - i.e. the commit on which reconcile has
already shifted children from the detached parent into the live tree.
Re-committed children absorb into the existing entry so
first_render=true survives a suspend/resume/suspend/resume cycle.
@Madoshakalaka Madoshakalaka merged commit 4021951 into master Apr 24, 2026
41 checks passed
@Madoshakalaka Madoshakalaka deleted the fix/suspense-use-effect-dom-ordering branch April 24, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew Area: The main yew crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling suspense::use_future makes children use_effect hooks execution inconsistent.

1 participant