Prerender to HTML under wasmtime, streaming into the response body - #20
Merged
Conversation
Adds `polyengine-dioxus-ssr`: the app's root component rendered by
`dioxus-ssr` rather than replayed through the mutation protocol, so
there is still exactly one implementation of the mutation semantics.
Two artifacts share `render_to`:
- SSG — a `wasi:cli/command` binary that writes HTML to stdout. No
HTTP, no async, no component-model streams; `wasmtime run` executes
it with no flags.
- serve — a `wasi:http/service` component behind the `serve` feature.
The serve path streams: `Renderer::render_to` writes through a
`BufWriter` into the response body's `stream<u8>` as it walks the tree.
That works because component-model async is more permissive than Rust
async — the sink is synchronous and calls `wit_bindgen::block_on`,
which wasmtime allows since `may_block` is
`task.async_function || task.returned_or_cancelled()` and
`wasi:http/handler.handle` is an `async func`. The render runs in a
`spawn_local` task after `task.return`, because `response.new` hands
the body's read end to the host and the host cannot read it until it
has the response.
Measured, since it is the opposite of what "blocking" suggests: six
concurrent rate-limited 341 KB responses finish in the wall time of
one at stock `wasmtime serve` settings, so no
`--max-instance-concurrent-reuse-count` tuning is needed.
The renderers are optional dependencies of the example, not just
optional code paths: `polyengine-dioxus` and `polyengine-dioxus-ssr`
each emit a `component-type` custom section and must never link into
one binary.
Gate ladder, each step adding one layer:
just test cargo test -p polyengine-dioxus-ssr — HTML
correctness, natively, no wasm or ports
just ssg-example the same golden file natively and under
`wasmtime run`
just serve-test only the wasi:http wrapper, on an ephemeral port
Hydration markers, suspense, server-data transport and routing are out
of scope; `pre_render` is one flag away but needs a client-side
binding walk to be worth anything.
lannbot
enabled auto-merge
September 4, 2026 15:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerenders a Dioxus app to HTML from a wasm component under wasmtime, with
no custom host:
wasmtime runfor the static artifact,wasmtime serveforthe HTTP one.
Approach
dioxus-ssrruns in the guest rather than replaying the mutationprotocol into a server-side DOM. That keeps exactly one implementation of
the mutation semantics (the existing WIT protocol +
host/src/applier.ts)instead of creating a second one that has to agree with it forever. It also
means
pre_render's hydration markers andwait_for_suspenseare one flagand one
.awaitaway when those tracks come up.dioxus-ssr0.7.9 depends only onaskama_escape/dioxus-core/dioxus-core-types/rustc-hash— no tokio, no net, no wasm-bindgen — soit compiles to
wasm32-wasip2unpatched.Two artifacts
wasi:cli/commandwasmtime run(no flags)wasi:http/servicewasmtime serve -S cliThey share
render_to. The renderers are optional dependencies of theexample, not just optional code paths:
polyengine-dioxusandpolyengine-dioxus-ssreach emit acomponent-typecustom section and mustnever link into one binary.
-S cliis not optional for the serve artifact: without it wasmtime linksonly the proxy world, and a rustc
wasm32-wasip2component importswasi:cli/environment,wasi:filesystem/preopensandexitthroughwasi-libc.
The serve path streams
Renderer::render_towrites through aBufWriterstraight into the responsebody's
stream<u8>as it walks the tree — no intermediateString.That is possible because component-model async is more permissive than Rust
async. The sink is a synchronous
io::Writethat callswit_bindgen::block_on; wasmtime permits blocking whenmay_block = task.async_function || task.returned_or_cancelled(), andwasi:http/handler.handleis anasync func. The render runs insidespawn_localaftertask.return, becauseresponse.newhands the body'sread end to the host and the host cannot read it until it has the
response — writing before returning would park forever.
Buffering is
std::io::BufWriter's job. The only hand-written piece is an8-line
fmt::Write→io::Writebridge, which exists becausefmt::Erroris a unit type and would otherwise destroy the real
io::Error(that is howa client hanging up mid-render is distinguished from success).
Measured, since it is the opposite of what "blocking" suggests: six
concurrent rate-limited 341 KB responses finish in the wall time of one at
stock
wasmtime servesettings. A task parked inwaitable-set.waitdoesnot hold off its instance-mates, so no
--max-instance-concurrent-reuse-counttuning is needed.
Gates
Each step adds exactly one layer; the expensive machinery is only at the top.
Not a gate: equality with what
host/src/applier.tsproduces in a browser.The applier sets
value/checked/selectedas JS properties, which haveno serialized form. dioxus-web lives with the same split because hydration
binds by markers, not by diffing output.
Sizes (counter example, release)
polymorph:dioxus/app)The 281 KB SSG→serve delta is the wasi:http/p3 async layer.
Out of scope
Hydration markers, suspense, server-data transport, routing, static assets.
Liveview still wants a custom host (websockets, persistent instance) but
shares nothing with this.