Skip to content

feat: out-of-order streaming SSR for Suspense#4031

Draft
Madoshakalaka wants to merge 5 commits intomasterfrom
feat/ssr-out-of-order-streaming
Draft

feat: out-of-order streaming SSR for Suspense#4031
Madoshakalaka wants to merge 5 commits intomasterfrom
feat/ssr-out-of-order-streaming

Conversation

@Madoshakalaka
Copy link
Member

@Madoshakalaka Madoshakalaka commented Mar 2, 2026

Description

We now stream suspense fallback UI immediately during SSR. So long running data-fetching won't bottleneck the page.

#[component]
fn ProductPage() -> HtmlResult {
    let data = use_prepared_state!(
        (),
        async move |_| -> String { fetch_from_database().await } // this takes 5 whopping seconds
    )?.unwrap();

    Ok(html! { <div>{data}</div> })
}

#[component]
fn App() -> Html {
    html! {
        <>
            <h1>{"My Store"}</h1>
            <Suspense fallback={html! { <p>{"Loading..."}</p> }}>
                <ProductPage />
            </Suspense>
            <footer>{"Footer streams immediately"}</footer>
        </>
    }
}

Before: the entire response blocks until fetch_from_database() completes.
After: <h1>, the fallback <p>Loading...</p>, and <footer> stream immediately. The resolved <div> swaps in when the fetch finishes.

Resolves #3619

Checklist

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

When a component inside a <Suspense> boundary suspends during SSR,
emit the fallback immediately and append the resolved content at the
end of the stream with an inline script that swaps it into place.
This avoids blocking the entire HTTP response on slow async operations.

- Add SsrContext to track deferred suspense boundaries
- Poll children once in VSuspense::render_into_stream; if Pending,
  stream fallback with boundary markers and spawn deferred rendering
- Flush deferred content with $YC swap script after main tree completes
- Thread SsrContext through all render_into_stream methods
- Add suspense_ssr example demonstrating the feature
@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Visit the preview URL for this PR (updated for commit 45a31f1):

https://yew-rs--pr4031-feat-ssr-out-of-orde-n7rapk68.web.app

(expires Mon, 09 Mar 2026 12:58:46 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.403 ns      │ 3.005 ns      │ 2.406 ns      │ 2.416 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.361 ns      │ 3.629 ns      │ 2.375 ns      │ 2.938 ns      │ 100     │ 1000000000

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.188 100.188 0 0.000%
boids 168.073 168.073 0 0.000%
communication_child_to_parent 93.464 93.464 0 0.000%
communication_grandchild_with_grandparent 105.240 105.240 0 0.000%
communication_grandparent_to_grandchild 101.603 101.603 0 0.000%
communication_parent_to_child 90.892 90.892 0 0.000%
contexts 105.142 105.142 0 0.000%
counter 86.274 86.274 0 0.000%
counter_functional 88.269 88.269 0 0.000%
dyn_create_destroy_apps 90.312 90.312 0 0.000%
file_upload 99.335 99.335 0 0.000%
function_delayed_input 94.362 94.362 0 0.000%
function_memory_game 172.937 172.937 0 0.000%
function_router 406.649 406.649 0 0.000%
function_todomvc 164.156 164.156 0 0.000%
futures 235.151 235.151 0 0.000%
game_of_life 104.709 104.709 0 0.000%
immutable 255.739 255.739 0 0.000%
inner_html 80.795 80.795 0 0.000%
js_callback 109.369 109.369 0 0.000%
keyed_list 179.717 179.717 0 0.000%
mount_point 84.139 84.139 0 0.000%
nested_list 113.048 113.048 0 0.000%
node_refs 91.518 91.518 0 0.000%
password_strength 1729.144 1729.144 0 0.000%
portals 93.027 93.027 0 0.000%
router 377.198 377.198 0 0.000%
suspense 113.450 113.450 0 0.000%
suspense_ssr N/A 219.973 N/A N/A
timer 88.625 88.625 0 0.000%
timer_functional 98.867 98.867 0 0.000%
todomvc 142.080 142.080 0 0.000%
two_apps 86.139 86.139 0 0.000%
web_worker_fib 136.215 136.215 0 0.000%
web_worker_prime 187.452 187.452 0 0.000%
webgl 83.217 83.217 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.550 314.212 311.216 1.117
Hello World 10 481.108 484.256 482.264 1.020
Function Router 10 31270.077 31870.057 31546.526 158.963
Concurrent Task 10 1005.690 1007.330 1006.703 0.523
Many Providers 10 1069.457 1109.531 1084.985 10.312

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.082 291.668 291.379 0.162
Hello World 10 483.369 531.510 493.834 15.461
Function Router 10 31653.098 32351.982 31944.797 228.698
Concurrent Task 10 1004.829 1006.892 1006.493 0.632
Many Providers 10 1065.969 1117.188 1085.643 16.333

In browser WASM, the scheduler defers via spawn_local, so poll!()
always returns Pending regardless of whether children truly suspend.
This caused hydration integration tests to fail because non-suspending
components were incorrectly rendered with fallback+deferred format.

Out-of-order streaming now only activates on native/WASI targets
where the scheduler is synchronous. On WASM-browser, the old inline
rendering behavior is preserved.
@Madoshakalaka Madoshakalaka added the A-yew Area: The main yew crate label Mar 2, 2026
this aligns with our template repo and works on both the frontend and backend
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.

SSR + hydration results in a stalled request

1 participant