The fastest SSR engine for Rust. Period.
Render 95,000+ pages per second with sub-millisecond latency. Drop-in replacement for Node.js SSR that's 50x faster.
┌─────────────────────────────────────────────────────────────┐
│ STRESS TEST (30 seconds) │
├─────────────────────────────────────────────────────────────┤
│ Requests/sec: 95,363 RPS │
│ Total requests: 2,869,878 │
│ Data transferred: 171 GB │
├─────────────────────────────────────────────────────────────┤
│ Latency p50: 0.46ms │
│ Latency p99: 4.60ms │
│ Max latency: 45.7ms │
└─────────────────────────────────────────────────────────────┘
| Engine | RPS | p99 Latency | Memory |
|---|---|---|---|
| Rusty SSR | 95,363 | 4.6ms | ~200MB |
| Next.js (Node) | 500-2,000 | 50-200ms | ~500MB+ |
| Nuxt (Node) | 500-1,500 | 40-150ms | ~500MB+ |
50x faster throughput. 40x lower latency. 60% less memory.
Node.js Cluster Mode Rusty SSR
┌─────────────────────┐ ┌─────────────────────┐
│ Process 1 │ │ 1 Process │
│ └─ V8 + 512MB heap │ │ ├─ V8 isolate 1 │
├─────────────────────┤ │ ├─ V8 isolate 2 │
│ Process 2 │ │ ├─ V8 isolate 3 │
│ └─ V8 + 512MB heap │ │ ├─ ... │
├─────────────────────┤ │ └─ V8 isolate 10 │
│ ... × 10 │ │ │
├─────────────────────┤ │ Shared L1/L2 Cache │
│ ~5GB RAM total │ │ ~200MB RAM total │
│ No shared cache │ │ Zero-copy Arc<str> │
└─────────────────────┘ └─────────────────────┘
- Node.js: 10 processes × 512MB = 5GB RAM, no shared cache
- Rusty SSR: 1 process, 10 V8 isolates, shared cache, 200MB RAM
Rusty SSR runs V8 isolates in a thread pool managed by Rust. Each CPU core gets its own V8 instance, but they share a common cache. Zero-copy Arc<str> means no memory duplication.
[dependencies]
rusty-ssr = "0.1"
tokio = { version = "1", features = ["full"] }
axum = "0.7"// ssr-bundle.js
globalThis.renderPage = async function(url, data) {
// Your framework's SSR here (React, Preact, Vue, Solid...)
const html = renderToString(<App url={url} {...data} />);
return `<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body><div id="app">${html}</div></body>
</html>`;
};use axum::{extract::State, response::Html, routing::get, Router};
use rusty_ssr::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() {
// Initialize SSR engine (auto-detects CPU cores)
let engine = Arc::new(
SsrEngine::builder()
.bundle_path("ssr-bundle.js")
.cache_size(500) // ~500 cached pages (entries, not MB)
.cache_ttl_secs(300) // 5 min TTL
.build_engine()
.expect("Failed to create SSR engine")
);
let app = Router::new()
.route("/", get(ssr_handler))
.route("/*path", get(ssr_handler))
.with_state(engine);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("SSR server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
async fn ssr_handler(
State(engine): State<Arc<SsrEngine>>,
axum::extract::Path(path): axum::extract::Path<String>,
) -> Html<String> {
match engine.render(&format!("/{}", path)).await {
Ok(html) => Html(html.to_string()),
Err(e) => Html(format!("<h1>Error</h1><pre>{}</pre>", e)),
}
}That's it. Your SSR is now 50x faster.
No more "window is not defined" errors. Rusty SSR automatically injects polyfills for:
window,document,navigator,locationlocalStorage,sessionStoragerequestAnimationFrame,cancelAnimationFrameMutationObserver,ResizeObserver,IntersectionObservermatchMedia,Image,performance
Just load your bundle — it works.
Request → L1/L2 Hot Cache (1-3ns) → Cold Cache (100ns) → V8 Render
↑ ↑ ↓
└──────────────────────────┴──── cache result ┘
- Hot cache: Thread-local, L1/L2 CPU cache speed
- Cold cache: DashMap with LRU eviction
- Automatic: No configuration needed
Works with any JavaScript framework that supports SSR:
- React / Preact
- Vue 3 / Nuxt
- Solid
- Svelte / SvelteKit
- Vanilla JS
See examples/bundles/ for complete examples.
// Simple render
let html = engine.render("/products").await?;
// With JSON data
use serde_json::json;
let html = engine.render_json("/products", json!({
"products": [...],
"user": { "id": 1 }
})).await?;
// With string data
let html = engine.render_with_data("/products", r#"{"page": 1}"#).await?;
// Skip cache (always render fresh)
let html = engine.render_uncached("/admin", "{}").await?;
// Assemble the full document in a single pass: the cached fragment goes
// into <!--ssr:outlet-->, and your per-request head tags into their own
// placeholders — one allocation, no chained String::replace.
let html = engine.render_to_html_with_replacements(
"/?listing=42",
"{}",
&[
("<!--ssr:title-->", "Listing #42"),
("<!--seo-->", "<meta property=\"og:title\" content=\"Listing #42\" />"),
],
).await?;Correctness, robustness and efficiency overhaul:
- Cache key covers URL and data, and the full key is compared on
lookup —
render_json(url, A)andrender_json(url, B)no longer collide, and a 64-bit hash collision degrades to a miss (never serves wrong content). - Errors/empty aren't frozen in cache: a render that throws returns
Err(uncached);.cache_empty(false)skips caching empty output. - Whole-request timeout:
request_timeoutbounds enqueue and the render wait, and a watchdog terminates a runaway render (even a non-allocatingwhile(true)) so the worker is reclaimed. A panicking render no longer kills its worker. - No per-request JS recompile: the render function is resolved once and invoked via a native call; URL/data are passed as V8 values (no source-escaping pitfalls).
- Real LRU hot cache (no FIFO drift / duplicate entries), V8 heap cap
(
.max_heap_mb), non-clobbering polyfills withURL/URLSearchParams(+.polyfills(false)), single-pass template assembly, and a.cache_key_normalizerfor collapsing tracking-param URLs.
One-time and tracking URLs (?reset=…, ?verify=…, ?utm_*, ?fbclid=…)
shouldn't each take a slot in a fixed-size cache. Two tools:
// Render fresh + assemble the template, but never touch the cache:
let html = engine
.render_to_html_uncached("/?reset=onetimetoken", "{}")
.await?;
// Or collapse equivalent URLs onto one cache key (strip the query):
fn strip_query(url: &str) -> String {
url.split('?').next().unwrap_or(url).to_string()
}
let engine = SsrEngine::builder()
.bundle_path("ssr-bundle.js")
.cache_key_normalizer(strip_query) // utm/fbclid variants now share one entry
.build_engine()?;On a small box, cap each isolate's heap. A render that exceeds it is
terminated and returns Err (uncached) instead of aborting the process:
let engine = SsrEngine::builder()
.bundle_path("ssr-bundle.js")
.max_heap_mb(256)
.build_engine()?; let engine = SsrEngine::builder()
.bundle_path("ssr-bundle.js") // Path to JS bundle
.pool_size(num_cpus::get()) // V8 workers (default: CPU count)
.queue_capacity(512) // Task queue size
.pin_threads(true) // Pin workers to CPU cores
.cache_size(500) // Number of cached entries
.cache_ttl_secs(300) // Cache TTL (0 = forever)
.cache_empty(false) // Don't cache empty renders (default: true)
.polyfills(true) // Built-in browser polyfills (default: true)
.max_heap_mb(512) // Per-isolate V8 heap cap (default: none)
.render_function("renderPage") // JS function name
.build_engine()?;let metrics = engine.cache_metrics();
println!("Hit rate: {:.1}%", metrics.hit_rate);
println!("Hot hits: {}", metrics.hot_hits);
println!("Cold hits: {}", metrics.cold_hits);
println!("Misses: {}", metrics.misses);// vite.config.ts
export default defineConfig({
build: {
ssr: true,
rollupOptions: {
input: 'src/entry-server.tsx',
output: {
format: 'iife',
name: 'SSRBundle',
inlineDynamicImports: true
},
},
},
});# Build SSR bundle
vite build --ssr
# Wrap for Rusty SSR
node scripts/build-bundle.js dist/server/entry.js ssr-bundle.js --iife SSRBundleWrite your bundle with globalThis.renderPage directly:
import { render } from 'preact-render-to-string';
import App from './App';
globalThis.renderPage = async function(url, data) {
const html = render(<App url={url} {...data} />);
return `<!DOCTYPE html><html><body>${html}</body></html>`;
};| Feature | Default | Description |
|---|---|---|
v8-pool |
✅ | V8 thread pool |
cache |
✅ | Multi-tier caching |
axum-integration |
✅ | Axum middleware |
brotli-compression |
❌ | Brotli middleware |
full |
❌ | All features |
# Minimal (just V8 pool)
rusty-ssr = { version = "0.1", default-features = false, features = ["v8-pool"] }
# Full (everything)
rusty-ssr = { version = "0.1", features = ["full"] }# Run all tests
cargo test
# Run integration tests only
cargo test --test integration_tests
# Run with verbose output
cargo test -- --nocaptureIntegration tests cover:
- V8 pool configuration
- DashMap concurrent cache operations
- LRU cache eviction behavior
- Async patterns (tokio channels, timeouts)
- Thread safety (Arc, Mutex, mpsc)
- URL parsing and JSON serialization
# Run all benchmarks
cargo bench
# Run SSR benchmarks only
cargo bench --bench ssr_benchmark
# Run cache benchmarks only
cargo bench --bench cache_benchmarkSSR Benchmarks (ssr_benchmark):
- Pool config creation overhead
- String operations (small/medium/large HTML)
- JSON serialization performance
- Channel throughput (request queue simulation)
Cache Benchmarks (cache_benchmark):
- DashMap concurrent read/write (1, 2, 4, 8 threads)
- DashMap sharding (sequential vs random keys)
- L1/L2 cache hit performance
- LRU eviction overhead (128, 512, 2048 entries)
- Arc vs String cloning
Results are saved to target/criterion/ with HTML reports.
┌─────────────────────────────────────────────────────────────┐
│ SsrEngine │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ SSR Cache │ │ V8 Pool │ │
│ │ │ miss │ │ │
│ │ ┌───────────┐ │ ───────► │ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │ Hot (L1) │ │ │ │ V8 │ │ V8 │ │ V8 │ │ │
│ │ └───────────┘ │ │ └────┘ └────┘ └────┘ │ │
│ │ ┌───────────┐ │ result │ ... │ │
│ │ │ Cold (RAM)│ │ ◄─────── │ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ └───────────┘ │ │ │ V8 │ │ V8 │ │ V8 │ │ │
│ │ LRU eviction │ │ └────┘ └────┘ └────┘ │ │
│ └─────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
FROM rust:1.75-slim-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/your-app /app/server
COPY ssr-bundle.js /app/
WORKDIR /app
CMD ["./server"]Just push your code — Rusty SSR works with any platform that supports Rust.
This shouldn't happen with v0.1+ — browser polyfills are automatic. If it does:
- Check your bundle doesn't run browser code at module load time
- Use
typeof window !== 'undefined'guards if needed
Your bundle must expose globalThis.renderPage:
// Correct
globalThis.renderPage = async (url, data) => { ... };
// Wrong
export function renderPage() { ... } // ESM export won't workSet a cache TTL to prevent unbounded growth:
.cache_ttl_secs(300) // Expire after 5 minutesMIT — use it however you want.
Issues and PRs welcome! See CONTRIBUTING.md.