A monorepo of young ideas
README.md

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 (see vite.config.js) produces two bundles:
    • dist/server.js — an SSR build emitted as an IIFE exposing globalThis.SSR.render. ssr.noExternal inlines React, and ssr.target: "webworker" resolves react-dom/server through the browser export condition so it has no Node dependencies.
    • dist/client.js — the hydration bundle served at /client.js.
  • ui/src/polyfills.js shims the handful of web globals React expects that QuickJS lacks (TextEncoder, MessageChannel, timers).
  • src/renderer.rs owns one QuickJS runtime with the server bundle evaluated, and installs a console that logs through Rust.
  • src/pool.rs runs one Renderer per worker thread (QuickJS is single-threaded) and dispatches jobs from async handlers over a channel.
  • src/main.rs is the Axum server. GET /?name=... renders App with those props and embeds them for hydration.
  • src/dev.rs provides the dev loop: a file watcher that hot-swaps the render pool when dist/server.js is 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 of renderToString, which would need a real ReadableStream implementation 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.