react_ssr #
An experiment in server-rendering React components from a Rust web server by
embedding the QuickJS JavaScript engine (via rquickjs).
How it works #
Axum handler ──props (JSON)──▶ RenderPool ──channel──▶ worker thread
│
QuickJS Runtime
└─ SSR.render(name, props)
└─ react-dom/server renderToString
│
HTML page ◀────────────────── html string ◀─────────────┘
ui/is a small pnpm project. Vite (seevite.config.js) produces two bundles:dist/server.js— an SSR build emitted as an IIFE exposingglobalThis.SSR.render.ssr.noExternalinlines React, andssr.target: "webworker"resolvesreact-dom/serverthrough thebrowserexport condition so it has no Node dependencies.dist/client.js— the hydration bundle served at/client.js.
ui/src/polyfills.jsshims the handful of web globals React expects that QuickJS lacks (TextEncoder,MessageChannel, timers).src/renderer.rsowns one QuickJS runtime with the server bundle evaluated, and installs aconsolethat logs through Rust.src/pool.rsruns oneRendererper worker thread (QuickJS is single-threaded) and dispatches jobs from async handlers over a channel.src/main.rsis the Axum server.GET /?name=...rendersAppwith those props and embeds them for hydration.src/dev.rsprovides the dev loop: a file watcher that hot-swaps the render pool whendist/server.jsis rebuilt, and the script tags that load the client through Vite's dev server.
Development (with Fast Refresh) #
Two processes:
# Terminal 1: Vite dev server (client, HMR) + SSR bundle watcher.
cd crates/react_ssr/ui
pnpm install
pnpm dev
# Terminal 2: the Rust server.
cargo run -p react_ssr
# Open http://127.0.0.1:3000/?name=Rustacean
Debug builds run in dev mode by default: the page loads src/client.jsx from
Vite at http://localhost:5173 (override with VITE_ORIGIN), so component
edits apply in the browser via React Fast Refresh with state preserved.
Meanwhile Vite rebuilds dist/server.js, and the Rust server notices and swaps
in fresh QuickJS workers so the next full page load renders and hydrates
against the same code. A bundle that fails to load is logged and the previous
workers keep serving.
Production build #
cd crates/react_ssr/ui && pnpm build
REACT_SSR_DEV=0 cargo run -p react_ssr # Or `cargo run --release`
In this mode the server reads both bundles from ui/dist at startup and serves
client.js itself. REACT_SSR_DEV=1 forces dev mode in a release build.
Open questions / next steps #
- Streaming (
renderToReadableStream) instead ofrenderToString, which would need a realReadableStreamimplementation in QuickJS. - React Server Components, which lean heavily on Node/edge runtime APIs.
- Snapshotting the initialised runtime (QuickJS bytecode) to speed up worker start-up.
- Per-request isolation vs. the current long-lived shared context.
- Hot-swapping the pool is also the shape of a zero-downtime bundle deploy; the same mechanism could be triggered by a signal or admin endpoint in production.