Run in cluster mode with n workers (uses react-server start)
+ *
+ * Runs autocannon against each benchmark route and prints a summary table.
+ */
+import { createServer } from "node:http";
+import { spawn } from "node:child_process";
+import { writeFileSync, readFileSync } from "node:fs";
+import { execSync } from "node:child_process";
+
+process.env.NODE_ENV = "production";
+
+// ── CLI args ─────────────────────────────────────────────────────────────────
+
+const args = process.argv.slice(2);
+const saveLabel = args.includes("--save")
+ ? args[args.indexOf("--save") + 1]
+ : null;
+const compareFile = args.includes("--compare")
+ ? args[args.indexOf("--compare") + 1]
+ : null;
+
+function parseCluster() {
+ const idx = args.findIndex((a) => a.startsWith("--cluster"));
+ if (idx === -1) return 0;
+ // --cluster=4 or --cluster 4
+ if (args[idx].includes("=")) return parseInt(args[idx].split("=")[1], 10);
+ return parseInt(args[idx + 1], 10);
+}
+const clusterSize = parseCluster();
+
+const PORT = 3210;
+const DURATION = 10; // seconds per test
+const CONNECTIONS = 50;
+
+// ── Boot production server ──────────────────────────────────────────────────
+
+let serverProcess = null;
+
+if (clusterSize > 0) {
+ // Cluster mode: spawn react-server start as a child process
+ serverProcess = await new Promise((resolve, reject) => {
+ const child = spawn(
+ "npx",
+ ["react-server", "start", "--port", String(PORT)],
+ {
+ stdio: ["pipe", "pipe", "pipe"],
+ env: {
+ ...process.env,
+ REACT_SERVER_CLUSTER: String(clusterSize),
+ },
+ }
+ );
+
+ let ready = false;
+ const onData = (chunk) => {
+ const text = chunk.toString();
+ process.stderr.write(text);
+ // Workers log "listening on" when ready — wait for all of them
+ if (!ready && text.includes("listening on")) {
+ ready = true;
+ // Give a moment for remaining workers to bind
+ setTimeout(() => resolve(child), 500);
+ }
+ };
+
+ child.stdout.on("data", onData);
+ child.stderr.on("data", onData);
+ child.on("error", reject);
+ child.on("exit", (code) => {
+ if (!ready)
+ reject(new Error(`react-server start exited with code ${code}`));
+ });
+
+ // Safety timeout
+ setTimeout(() => {
+ if (!ready) {
+ child.kill();
+ reject(new Error("Timed out waiting for cluster to start"));
+ }
+ }, 30000);
+ });
+} else {
+ // Single-process mode: use programmatic middleware API
+ const { reactServer } = await import("@lazarv/react-server/node");
+ const { middlewares } = await reactServer({
+ origin: `http://localhost:${PORT}`,
+ host: "localhost",
+ port: PORT,
+ outDir: ".react-server",
+ });
+
+ const server = createServer(middlewares);
+ await new Promise((resolve) => server.listen(PORT, resolve));
+ // Store for cleanup
+ serverProcess = server;
+}
+
+const mode =
+ clusterSize > 0 ? `cluster (${clusterSize} workers)` : "single-process";
+console.log(`\nBenchmark server on http://localhost:${PORT} [${mode}]`);
+console.log(`Running ${DURATION}s per test, ${CONNECTIONS} connections\n`);
+
+// ── Benchmark definitions ───────────────────────────────────────────────────
+
+const BENCHMARKS = [
+ { name: "minimal", path: "/", desc: "Minimal SSR (tiny page)" },
+ { name: "small", path: "/small", desc: "Small SSR (~20 elements)" },
+ { name: "medium", path: "/medium", desc: "Medium SSR (50 products)" },
+ { name: "large", path: "/large", desc: "Large SSR (500-row table)" },
+ { name: "deep", path: "/deep", desc: "Deep nesting (100 levels)" },
+ { name: "wide", path: "/wide", desc: "Wide tree (1000 siblings)" },
+ { name: "cached", path: "/cached", desc: "Cached medium page" },
+ {
+ name: "client-min",
+ path: "/client",
+ desc: "Client component minimal",
+ },
+ {
+ name: "client-small",
+ path: "/client/small",
+ desc: "Client component small",
+ },
+ {
+ name: "client-med",
+ path: "/client/medium",
+ desc: "Client component medium (50 products)",
+ },
+ {
+ name: "client-large",
+ path: "/client/large",
+ desc: "Client component large (500 rows)",
+ },
+ {
+ name: "client-deep",
+ path: "/client/deep",
+ desc: "Client component deep (100 levels)",
+ },
+ {
+ name: "client-wide",
+ path: "/client/wide",
+ desc: "Client component wide (1000 siblings)",
+ },
+ {
+ name: "static-json",
+ path: "/data.json",
+ desc: "Static file (JSON)",
+ },
+ {
+ name: "static-js",
+ path: null, // resolved dynamically
+ desc: "Static file (JS bundle)",
+ },
+ { name: "404-miss", path: "/nonexistent", desc: "404 miss → SSR" },
+];
+
+// ── Find an actual JS bundle path ───────────────────────────────────────────
+
+import { readdirSync } from "node:fs";
+try {
+ const clientFiles = readdirSync(".react-server/client");
+ const jsBundle = clientFiles.find(
+ (f) => f.endsWith(".mjs") && f.includes(".")
+ );
+ if (jsBundle) {
+ BENCHMARKS.find((b) => b.name === "static-js").path = `/client/${jsBundle}`;
+ }
+} catch {
+ // skip if not found
+}
+
+// ── Run autocannon ──────────────────────────────────────────────────────────
+
+async function runAutocannon(url, extraArgs = []) {
+ return new Promise((resolve, reject) => {
+ const args = [
+ "autocannon",
+ "-c",
+ String(CONNECTIONS),
+ "-d",
+ String(DURATION),
+ "--json",
+ ...extraArgs,
+ url,
+ ];
+ const proc = spawn("npx", args, { stdio: ["pipe", "pipe", "pipe"] });
+ let stdout = "";
+ proc.stdout.on("data", (d) => (stdout += d));
+ proc.on("close", (code) => {
+ try {
+ resolve(JSON.parse(stdout));
+ } catch {
+ reject(new Error(`autocannon failed (code ${code}): ${stdout}`));
+ }
+ });
+ proc.on("error", reject);
+ });
+}
+
+// ── Warm up ─────────────────────────────────────────────────────────────────
+
+console.log("Warming up...");
+for (const b of BENCHMARKS) {
+ if (!b.path) continue;
+ try {
+ await fetch(`http://localhost:${PORT}${b.path}`);
+ } catch {
+ // ignore
+ }
+}
+// Second pass to ensure caches are primed
+for (const b of BENCHMARKS) {
+ if (!b.path) continue;
+ try {
+ await fetch(`http://localhost:${PORT}${b.path}`);
+ } catch {
+ // ignore
+ }
+}
+console.log("Warm-up done.\n");
+
+// ── Run benchmarks ──────────────────────────────────────────────────────────
+
+const results = [];
+
+for (const b of BENCHMARKS) {
+ if (!b.path) {
+ console.log(`⏭ Skipping ${b.name} (no path resolved)`);
+ continue;
+ }
+
+ process.stdout.write(`▶ ${b.name.padEnd(14)} ${b.desc}...`);
+ const url = `http://localhost:${PORT}${b.path}`;
+ const data = await runAutocannon(url);
+
+ const result = {
+ name: b.name,
+ desc: b.desc,
+ path: b.path,
+ reqSec: data.requests.average,
+ latencyAvg: data.latency.average,
+ latencyP50: data.latency.p50,
+ latencyP99: data.latency.p99,
+ throughputMB: (data.throughput.average / 1024 / 1024).toFixed(1),
+ total2xx: data["2xx"],
+ errors: data.errors,
+ };
+ results.push(result);
+
+ console.log(
+ ` ${result.reqSec.toFixed(0)} req/s | avg ${result.latencyAvg}ms | p99 ${result.latencyP99}ms`
+ );
+}
+
+// ── Summary table ───────────────────────────────────────────────────────────
+
+// Load comparison data if requested
+let compareData = null;
+if (compareFile) {
+ try {
+ const raw = JSON.parse(readFileSync(compareFile, "utf8"));
+ compareData = new Map(raw.results.map((r) => [r.name, r]));
+ console.log(`\nComparing against: ${raw.description || compareFile}\n`);
+ } catch (e) {
+ console.warn(`Warning: could not load compare file: ${e.message}\n`);
+ }
+}
+
+function fmtDelta(current, baseline, lowerIsBetter = false) {
+ if (baseline == null || baseline === 0) return "";
+ const pct = ((current - baseline) / baseline) * 100;
+ const sign = pct > 0 ? "+" : "";
+ const good = lowerIsBetter ? pct < 0 : pct > 0;
+ const arrow = good ? "▲" : pct === 0 ? "=" : "▼";
+ return ` ${arrow}${sign}${pct.toFixed(0)}%`;
+}
+
+if (compareData) {
+ console.log("\n" + "═".repeat(130));
+ console.log(
+ " " +
+ "Benchmark".padEnd(16) +
+ "Req/s".padStart(16) +
+ "Avg (ms)".padStart(16) +
+ "P50 (ms)".padStart(14) +
+ "P99 (ms)".padStart(14) +
+ "Throughput".padStart(12) +
+ " " +
+ "Description"
+ );
+ console.log("─".repeat(130));
+ for (const r of results) {
+ const base = compareData.get(r.name);
+ console.log(
+ " " +
+ r.name.padEnd(16) +
+ (r.reqSec.toFixed(0) + fmtDelta(r.reqSec, base?.reqSec)).padStart(16) +
+ (
+ r.latencyAvg + fmtDelta(r.latencyAvg, base?.latencyAvg, true)
+ ).padStart(16) +
+ String(r.latencyP50).padStart(14) +
+ String(r.latencyP99).padStart(14) +
+ `${r.throughputMB} MB/s`.padStart(12) +
+ " " +
+ r.desc
+ );
+ }
+ console.log("═".repeat(130));
+} else {
+ console.log("\n" + "═".repeat(110));
+ console.log(
+ " " +
+ "Benchmark".padEnd(16) +
+ "Req/s".padStart(10) +
+ "Avg (ms)".padStart(10) +
+ "P50 (ms)".padStart(10) +
+ "P99 (ms)".padStart(10) +
+ "Throughput".padStart(12) +
+ " " +
+ "Description"
+ );
+ console.log("─".repeat(110));
+ for (const r of results) {
+ console.log(
+ " " +
+ r.name.padEnd(16) +
+ String(r.reqSec.toFixed(0)).padStart(10) +
+ String(r.latencyAvg).padStart(10) +
+ String(r.latencyP50).padStart(10) +
+ String(r.latencyP99).padStart(10) +
+ `${r.throughputMB} MB/s`.padStart(12) +
+ " " +
+ r.desc
+ );
+ }
+ console.log("═".repeat(110));
+}
+
+// ── Save results ─────────────────────────────────────────────────────────────
+
+if (saveLabel) {
+ let gitCommit = "unknown";
+ try {
+ gitCommit = execSync("git log --oneline -1", { encoding: "utf8" }).trim();
+ } catch {
+ // ignore
+ }
+ const config = { duration: DURATION, connections: CONNECTIONS, port: PORT };
+ if (clusterSize > 0) config.cluster = clusterSize;
+ const output = {
+ description: saveLabel,
+ date: new Date().toISOString().slice(0, 10),
+ commit: gitCommit,
+ config,
+ results,
+ };
+ const filename = `results-${saveLabel.replace(/[^a-zA-Z0-9_-]/g, "-")}.json`;
+ writeFileSync(filename, JSON.stringify(output, null, 2) + "\n");
+ console.log(`\nResults saved to ${filename}`);
+}
+
+if (serverProcess && typeof serverProcess.close === "function") {
+ // Single-process mode: HTTP server
+ serverProcess.close();
+} else if (serverProcess && typeof serverProcess.kill === "function") {
+ // Cluster mode: child process
+ serverProcess.kill("SIGTERM");
+}
+process.exit(0);
diff --git a/examples/benchmark/package.json b/examples/benchmark/package.json
new file mode 100644
index 00000000..e46a2aeb
--- /dev/null
+++ b/examples/benchmark/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@lazarv/react-server-example-benchmark",
+ "private": true,
+ "description": "Performance benchmark example for @lazarv/react-server",
+ "scripts": {
+ "dev": "react-server",
+ "build": "react-server build",
+ "start": "react-server start",
+ "bench": "node bench.mjs"
+ },
+ "dependencies": {
+ "@lazarv/react-server": "workspace:^"
+ }
+}
diff --git a/examples/benchmark/pages/(root).layout.jsx b/examples/benchmark/pages/(root).layout.jsx
new file mode 100644
index 00000000..c65a987e
--- /dev/null
+++ b/examples/benchmark/pages/(root).layout.jsx
@@ -0,0 +1,12 @@
+export default function RootLayout({ children }) {
+ return (
+
+
+
+
+ Benchmark
+
+ {children}
+
+ );
+}
diff --git a/examples/benchmark/pages/cached.jsx b/examples/benchmark/pages/cached.jsx
new file mode 100644
index 00000000..40082857
--- /dev/null
+++ b/examples/benchmark/pages/cached.jsx
@@ -0,0 +1,42 @@
+/**
+ * Cached page — same content as /medium but with response caching enabled.
+ * Measures the response cache fast path vs uncached rendering.
+ */
+import { useResponseCache } from "@lazarv/react-server";
+
+function ProductCard({ id }) {
+ return (
+
+
+ Product {id}
+
+ High-quality item with excellent features. Perfect for everyday use.
+ Rating: {(id % 5) + 1}/5 stars.
+
+
+ ${((id * 17 + 29) % 200) + 9.99}
+ {id % 3 === 0 && (On Sale)}
+
+
+
+ );
+}
+
+export default function Cached() {
+ useResponseCache(60000);
+
+ const products = Array.from({ length: 50 }, (_, i) => i + 1);
+ return (
+
+
+
+ {products.map((id) => (
+
+ ))}
+
+
+ );
+}
diff --git a/examples/benchmark/pages/client/deep.jsx b/examples/benchmark/pages/client/deep.jsx
new file mode 100644
index 00000000..5c5f1f80
--- /dev/null
+++ b/examples/benchmark/pages/client/deep.jsx
@@ -0,0 +1,26 @@
+"use client";
+
+/**
+ * Deep nesting page — client component SSR path.
+ * Same as /deep but rendered as a client component.
+ */
+
+function Wrapper({ depth, children }) {
+ if (depth <= 0) return {children}
;
+ return (
+
+ {children}
+
+ );
+}
+
+export default function Deep() {
+ return (
+
+ Deep Nesting (100 levels)
+
+ Leaf node at the bottom of the tree.
+
+
+ );
+}
diff --git a/examples/benchmark/pages/client/index.jsx b/examples/benchmark/pages/client/index.jsx
new file mode 100644
index 00000000..67b32a8a
--- /dev/null
+++ b/examples/benchmark/pages/client/index.jsx
@@ -0,0 +1,9 @@
+"use client";
+
+/**
+ * Index page — minimal SSR via client component path.
+ * Same as / but rendered as a client component (no RSC Flight serialization).
+ */
+export default function Index() {
+ return Benchmark
;
+}
diff --git a/examples/benchmark/pages/client/large.jsx b/examples/benchmark/pages/client/large.jsx
new file mode 100644
index 00000000..a2c31d6d
--- /dev/null
+++ b/examples/benchmark/pages/client/large.jsx
@@ -0,0 +1,89 @@
+"use client";
+
+/**
+ * Large page — client component SSR path.
+ * Same as /large but rendered as a client component.
+ */
+
+const COLUMNS = [
+ "ID",
+ "Name",
+ "Email",
+ "Department",
+ "Role",
+ "Status",
+ "Joined",
+ "Score",
+];
+
+const DEPARTMENTS = [
+ "Engineering",
+ "Marketing",
+ "Sales",
+ "Support",
+ "Design",
+ "Product",
+ "Finance",
+ "Legal",
+];
+const ROLES = [
+ "Manager",
+ "Senior",
+ "Junior",
+ "Lead",
+ "Director",
+ "Intern",
+ "Principal",
+ "Staff",
+];
+const STATUSES = ["Active", "Inactive", "On Leave", "Probation"];
+
+function TableRow({ i }) {
+ const dept = DEPARTMENTS[i % DEPARTMENTS.length];
+ const role = ROLES[i % ROLES.length];
+ const status = STATUSES[i % STATUSES.length];
+ const score = ((i * 7 + 13) % 100) + 1;
+ return (
+
+ | {i} |
+
+ User {i} {dept[0]}
+ |
+ user{i}@example.com |
+ {dept} |
+ {role} |
+ {status} |
+
+ 2024-{String((i % 12) + 1).padStart(2, "0")}-
+ {String((i % 28) + 1).padStart(2, "0")}
+ |
+ {score} |
+
+ );
+}
+
+export default function Large() {
+ const rows = Array.from({ length: 500 }, (_, i) => i + 1);
+ return (
+
+
+ Employee Directory
+ {rows.length} records
+
+
+
+
+ {COLUMNS.map((col) => (
+ | {col} |
+ ))}
+
+
+
+ {rows.map((i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/examples/benchmark/pages/client/medium.jsx b/examples/benchmark/pages/client/medium.jsx
new file mode 100644
index 00000000..88983d62
--- /dev/null
+++ b/examples/benchmark/pages/client/medium.jsx
@@ -0,0 +1,41 @@
+"use client";
+
+/**
+ * Medium page — client component SSR path.
+ * Same as /medium but rendered as a client component.
+ */
+
+function ProductCard({ id }) {
+ return (
+
+
+ Product {id}
+
+ High-quality item with excellent features. Perfect for everyday use.
+ Rating: {(id % 5) + 1}/5 stars.
+
+
+ ${((id * 17 + 29) % 200) + 9.99}
+ {id % 3 === 0 && (On Sale)}
+
+
+
+ );
+}
+
+export default function Medium() {
+ const products = Array.from({ length: 50 }, (_, i) => i + 1);
+ return (
+
+
+
+ {products.map((id) => (
+
+ ))}
+
+
+ );
+}
diff --git a/examples/benchmark/pages/client/small.jsx b/examples/benchmark/pages/client/small.jsx
new file mode 100644
index 00000000..b9f0fad8
--- /dev/null
+++ b/examples/benchmark/pages/client/small.jsx
@@ -0,0 +1,33 @@
+"use client";
+
+/**
+ * Small page — client component SSR path.
+ * Same as /small but rendered as a client component.
+ */
+export default function Small() {
+ return (
+
+
+
+ Welcome to Our Platform
+
+ Build modern web applications with server components. Fast, reliable,
+ and easy to use.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/benchmark/pages/client/wide.jsx b/examples/benchmark/pages/client/wide.jsx
new file mode 100644
index 00000000..95382242
--- /dev/null
+++ b/examples/benchmark/pages/client/wide.jsx
@@ -0,0 +1,28 @@
+"use client";
+
+/**
+ * Wide page — client component SSR path.
+ * Same as /wide but rendered as a client component.
+ */
+
+function Item({ i }) {
+ return (
+
+ Item #{i} — {i % 2 === 0 ? "even" : "odd"}
+
+ );
+}
+
+export default function Wide() {
+ const items = Array.from({ length: 1000 }, (_, i) => i + 1);
+ return (
+
+ Wide Tree (1000 siblings)
+
+ {items.map((i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/examples/benchmark/pages/deep.jsx b/examples/benchmark/pages/deep.jsx
new file mode 100644
index 00000000..573a8286
--- /dev/null
+++ b/examples/benchmark/pages/deep.jsx
@@ -0,0 +1,25 @@
+/**
+ * Deep nesting page — 100 levels of component nesting.
+ * Tests React reconciler / RSC serialization overhead with deep trees.
+ * Small HTML output but deep virtual DOM.
+ */
+
+function Wrapper({ depth, children }) {
+ if (depth <= 0) return {children}
;
+ return (
+
+ {children}
+
+ );
+}
+
+export default function Deep() {
+ return (
+
+ Deep Nesting (100 levels)
+
+ Leaf node at the bottom of the tree.
+
+
+ );
+}
diff --git a/examples/benchmark/pages/index.jsx b/examples/benchmark/pages/index.jsx
new file mode 100644
index 00000000..d7b05c7d
--- /dev/null
+++ b/examples/benchmark/pages/index.jsx
@@ -0,0 +1,7 @@
+/**
+ * Index page — minimal SSR, no data, tiny HTML.
+ * Baseline for measuring framework overhead.
+ */
+export default function Index() {
+ return Benchmark
;
+}
diff --git a/examples/benchmark/pages/large.jsx b/examples/benchmark/pages/large.jsx
new file mode 100644
index 00000000..66085538
--- /dev/null
+++ b/examples/benchmark/pages/large.jsx
@@ -0,0 +1,87 @@
+/**
+ * Large page — a data table with 500 rows × 8 columns.
+ * ~4000+ elements, ~80KB+ HTML. Typical admin dashboard / report.
+ */
+
+const COLUMNS = [
+ "ID",
+ "Name",
+ "Email",
+ "Department",
+ "Role",
+ "Status",
+ "Joined",
+ "Score",
+];
+
+const DEPARTMENTS = [
+ "Engineering",
+ "Marketing",
+ "Sales",
+ "Support",
+ "Design",
+ "Product",
+ "Finance",
+ "Legal",
+];
+const ROLES = [
+ "Manager",
+ "Senior",
+ "Junior",
+ "Lead",
+ "Director",
+ "Intern",
+ "Principal",
+ "Staff",
+];
+const STATUSES = ["Active", "Inactive", "On Leave", "Probation"];
+
+function TableRow({ i }) {
+ const dept = DEPARTMENTS[i % DEPARTMENTS.length];
+ const role = ROLES[i % ROLES.length];
+ const status = STATUSES[i % STATUSES.length];
+ const score = ((i * 7 + 13) % 100) + 1;
+ return (
+
+ | {i} |
+
+ User {i} {dept[0]}
+ |
+ user{i}@example.com |
+ {dept} |
+ {role} |
+ {status} |
+
+ 2024-{String((i % 12) + 1).padStart(2, "0")}-
+ {String((i % 28) + 1).padStart(2, "0")}
+ |
+ {score} |
+
+ );
+}
+
+export default function Large() {
+ const rows = Array.from({ length: 500 }, (_, i) => i + 1);
+ return (
+
+
+ Employee Directory
+ {rows.length} records
+
+
+
+
+ {COLUMNS.map((col) => (
+ | {col} |
+ ))}
+
+
+
+ {rows.map((i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/examples/benchmark/pages/medium.jsx b/examples/benchmark/pages/medium.jsx
new file mode 100644
index 00000000..a200f1fa
--- /dev/null
+++ b/examples/benchmark/pages/medium.jsx
@@ -0,0 +1,39 @@
+/**
+ * Medium page — a product listing with 50 items.
+ * ~200 elements, ~10KB HTML. Typical e-commerce or dashboard page.
+ */
+
+function ProductCard({ id }) {
+ return (
+
+
+ Product {id}
+
+ High-quality item with excellent features. Perfect for everyday use.
+ Rating: {(id % 5) + 1}/5 stars.
+
+
+ ${((id * 17 + 29) % 200) + 9.99}
+ {id % 3 === 0 && (On Sale)}
+
+
+
+ );
+}
+
+export default function Medium() {
+ const products = Array.from({ length: 50 }, (_, i) => i + 1);
+ return (
+
+
+
+ {products.map((id) => (
+
+ ))}
+
+
+ );
+}
diff --git a/examples/benchmark/pages/small.jsx b/examples/benchmark/pages/small.jsx
new file mode 100644
index 00000000..5ed57544
--- /dev/null
+++ b/examples/benchmark/pages/small.jsx
@@ -0,0 +1,31 @@
+/**
+ * Small page — a handful of elements, typical for a landing page hero section.
+ * ~20 elements, ~1KB HTML.
+ */
+export default function Small() {
+ return (
+
+
+
+ Welcome to Our Platform
+
+ Build modern web applications with server components. Fast, reliable,
+ and easy to use.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/benchmark/pages/wide.jsx b/examples/benchmark/pages/wide.jsx
new file mode 100644
index 00000000..64911c9b
--- /dev/null
+++ b/examples/benchmark/pages/wide.jsx
@@ -0,0 +1,27 @@
+/**
+ * Wide page — 1000 sibling components.
+ * Tests RSC serialization and HTML rendering with broad, flat trees.
+ * ~3000 elements, ~30KB HTML.
+ */
+
+function Item({ i }) {
+ return (
+
+ Item #{i} — {i % 2 === 0 ? "even" : "odd"}
+
+ );
+}
+
+export default function Wide() {
+ const items = Array.from({ length: 1000 }, (_, i) => i + 1);
+ return (
+
+ Wide Tree (1000 siblings)
+
+ {items.map((i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/examples/benchmark/public/data.json b/examples/benchmark/public/data.json
new file mode 100644
index 00000000..3ce57983
--- /dev/null
+++ b/examples/benchmark/public/data.json
@@ -0,0 +1,5 @@
+{
+ "benchmark": true,
+ "items": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "description": "Static JSON file for benchmarking static file serving performance."
+}
diff --git a/examples/benchmark/react-server.config.mjs b/examples/benchmark/react-server.config.mjs
new file mode 100644
index 00000000..c59a1749
--- /dev/null
+++ b/examples/benchmark/react-server.config.mjs
@@ -0,0 +1,3 @@
+export default {
+ root: "pages",
+};
diff --git a/packages/react-server/lib/handlers/static.mjs b/packages/react-server/lib/handlers/static.mjs
index 0a2aa229..0cb88c51 100644
--- a/packages/react-server/lib/handlers/static.mjs
+++ b/packages/react-server/lib/handlers/static.mjs
@@ -10,6 +10,7 @@ import {
POSTPONE_STATE,
PRELUDE_HTML,
PRERENDER_CACHE_DATA,
+ RESPONSE_BUFFER,
} from "../../server/symbols.mjs";
import * as sys from "../sys.mjs";
@@ -17,11 +18,15 @@ const cwd = sys.cwd();
export default async function staticHandler(dir, options = {}) {
const files = new Map();
+ const misses = new Set();
const exists = (path) => {
if (files.has(path)) {
return true;
}
+ if (misses.has(path)) {
+ return false;
+ }
try {
const file = statSync(join(cwd, options.cwd ?? ".", path));
if (file.isFile()) {
@@ -40,6 +45,7 @@ export default async function staticHandler(dir, options = {}) {
} catch {
// ignore
}
+ misses.add(path);
return false;
};
@@ -53,14 +59,22 @@ export default async function staticHandler(dir, options = {}) {
let { pathname } = context.url;
let contentEncoding = undefined;
- let prelude = null;
- const acceptEncoding = context.request.headers.get("accept-encoding");
- const isBrotli = acceptEncoding?.includes("br");
- const isGzip = acceptEncoding?.includes("gzip");
+ // Resolve the file: try the path directly, then as /index.html
+ let basename;
+ if (exists(pathname)) {
+ basename = pathname;
+ } else {
+ const indexPath = `${pathname}/index.html`.replace(/^\/+/g, "/");
+ if (exists(indexPath)) {
+ basename = indexPath;
+ } else {
+ // Neither the path nor its index.html exist in this handler's directory.
+ // Bail out early — no point checking postponed/compressed variants.
+ return;
+ }
+ }
- const basename = (
- exists(pathname) ? pathname : `${pathname}/index.html`
- ).replace(/^\/+/g, "/");
+ let prelude = null;
if (exists(`${basename}.postponed.json`)) {
prelude = basename;
pathname = basename;
@@ -80,14 +94,20 @@ export default async function staticHandler(dir, options = {}) {
]);
prerender$(POSTPONE_STATE, postponed);
prerender$(PRERENDER_CACHE_DATA, cacheData);
- } else if (isBrotli && exists(`${basename}.br`)) {
- pathname = `${basename}.br`;
- contentEncoding = "br";
- } else if (isGzip && exists(`${basename}.gz`)) {
- pathname = `${basename}.gz`;
- contentEncoding = "gzip";
- } else if (exists(basename)) {
- pathname = basename;
+ } else {
+ const acceptEncoding = context.request.headers.get("accept-encoding");
+ const isBrotli = acceptEncoding?.includes("br");
+ const isGzip = acceptEncoding?.includes("gzip");
+
+ if (isBrotli && exists(`${basename}.br`)) {
+ pathname = `${basename}.br`;
+ contentEncoding = "br";
+ } else if (isGzip && exists(`${basename}.gz`)) {
+ pathname = `${basename}.gz`;
+ contentEncoding = "gzip";
+ } else {
+ pathname = basename;
+ }
}
if (pathname !== "/" && exists(pathname)) {
@@ -141,7 +161,7 @@ export default async function staticHandler(dir, options = {}) {
prerender$(PRELUDE_HTML, res);
return;
}
- return new Response(res, {
+ const response = new Response(res, {
headers: {
"content-type":
file.mime.includes("text/") || file.mime === "application/json"
@@ -160,6 +180,10 @@ export default async function staticHandler(dir, options = {}) {
...(contentEncoding && { "content-encoding": contentEncoding }),
},
});
+ if (!(res instanceof ReadableStream)) {
+ response[RESPONSE_BUFFER] = res;
+ }
+ return response;
}
} catch (error) {
if (error.code !== "ENOENT") {
diff --git a/packages/react-server/lib/http/middleware.mjs b/packages/react-server/lib/http/middleware.mjs
index 428ac39a..a3f24682 100644
--- a/packages/react-server/lib/http/middleware.mjs
+++ b/packages/react-server/lib/http/middleware.mjs
@@ -6,7 +6,11 @@ import { isDeno } from "../sys.mjs";
import { compose } from "./middlewares/compose.mjs";
import { ContextStorage } from "../../server/context.mjs";
import { getRuntime } from "../../server/runtime.mjs";
-import { AFTER_CONTEXT, LOGGER_CONTEXT } from "../../server/symbols.mjs";
+import {
+ AFTER_CONTEXT,
+ LOGGER_CONTEXT,
+ RESPONSE_BUFFER,
+} from "../../server/symbols.mjs";
import {
getMetrics,
startRequestSpan,
@@ -170,48 +174,41 @@ export function createMiddleware(handler, options = {}) {
}
return;
}
- // Convert the Web ReadableStream to a Node Readable and pipe into ServerResponse.
- // Use AbortController to coordinate cleanup when client disconnects or stream completes.
- const nodeReadable = Readable.fromWeb(response.body);
+ // Fast path: buffer-backed responses skip stream conversion entirely.
+ // Responses tagged with RESPONSE_BUFFER already have their full body in memory.
+ const directBuffer = response[RESPONSE_BUFFER];
+ if (directBuffer) {
+ res.end(Buffer.from(directBuffer));
+ } else {
+ // Convert the Web ReadableStream to a Node Readable and pipe into ServerResponse.
+ const nodeReadable = Readable.fromWeb(response.body);
- // Destroy stream when aborted (client disconnect or error)
- signal.addEventListener(
- "abort",
- () => {
- try {
- nodeReadable.destroy(new Error("aborted"));
- } catch {
- // no-op
+ // Handle client disconnect: abort the signal (for useSignal() consumers)
+ // and destroy the readable. Only fires on premature close — on successful
+ // completion the listener is removed before "close" fires, so no
+ // DOMException is constructed on the happy path.
+ const onClose = () => {
+ if (!res.writableFinished) {
+ abortController.abort();
+ try {
+ nodeReadable.destroy(new Error("aborted"));
+ } catch {
+ // no-op
+ }
}
- },
- { once: true }
- );
-
- // Abort on client disconnect
- const onDisconnect = () => abortController.abort();
- res.once("close", onDisconnect);
- req.once("aborted", onDisconnect);
-
- try {
- await new Promise((resolve, reject) => {
- // Use { once: true } for auto-cleanup
- const onFinish = () => resolve();
- const onReadableError = (err) => reject(err);
- const onResError = (err) => reject(err);
-
- nodeReadable.once("error", onReadableError);
- res.once("error", onResError);
- res.once("finish", onFinish);
+ };
+ res.once("close", onClose);
- // End dest when source ends (default true)
- nodeReadable.pipe(res);
- });
- } finally {
- // Trigger abort to clean up the signal listener
- abortController.abort();
- // Remove disconnect listeners
- res.off("close", onDisconnect);
- req.off("aborted", onDisconnect);
+ try {
+ await new Promise((resolve, reject) => {
+ nodeReadable.once("error", reject);
+ res.once("error", reject);
+ res.once("finish", resolve);
+ nodeReadable.pipe(res);
+ });
+ } finally {
+ res.off("close", onClose);
+ }
}
// ── Telemetry: finish root span and record metrics ──
diff --git a/packages/react-server/lib/http/middlewares/compose.mjs b/packages/react-server/lib/http/middlewares/compose.mjs
index 3dfe5ce9..03b6c5e5 100644
--- a/packages/react-server/lib/http/middlewares/compose.mjs
+++ b/packages/react-server/lib/http/middlewares/compose.mjs
@@ -2,6 +2,7 @@ import {
getTracer,
getOtelContext,
makeSpanContext,
+ isTracingEnabled,
} from "../../../server/telemetry.mjs";
// ── Human-readable middleware display names ──
@@ -41,6 +42,13 @@ export function compose(middlewares) {
if (!fn) return undefined;
context.next = () => dispatch(i + 1);
+ // Fast path: skip all span machinery when telemetry is disabled
+ if (!isTracingEnabled()) {
+ const result = await fn(context);
+ if (result === undefined) return context.next();
+ return result;
+ }
+
// ── Telemetry: per-middleware span ──
const tracer = getTracer();
const parentCtx = context._otelCtx ?? getOtelContext();
diff --git a/packages/react-server/lib/loader/node-loader.mjs b/packages/react-server/lib/loader/node-loader.mjs
index 9bea8030..1d5007c3 100644
--- a/packages/react-server/lib/loader/node-loader.mjs
+++ b/packages/react-server/lib/loader/node-loader.mjs
@@ -10,12 +10,23 @@ const alias = moduleAliases();
const cwd = sys.cwd();
let options, outDir;
+const resolveCache = new Map();
export async function initialize(data) {
options = data?.options || {};
outDir = options.outDir || ".react-server";
}
export async function resolve(specifier, context, nextResolve) {
+ const cacheKey = specifier + "\0" + (context.parentURL ?? "");
+ const cached = resolveCache.get(cacheKey);
+ if (cached) return { ...cached, shortCircuit: true };
+
+ const result = await resolveUncached(specifier, context, nextResolve);
+ resolveCache.set(cacheKey, result);
+ return result;
+}
+
+async function resolveUncached(specifier, context, nextResolve) {
switch (specifier) {
case "@lazarv/react-server/dist/server/preload-manifest":
return nextResolve(
diff --git a/packages/react-server/lib/loader/node-loader.react-server.mjs b/packages/react-server/lib/loader/node-loader.react-server.mjs
index 3735bbf8..22933f44 100644
--- a/packages/react-server/lib/loader/node-loader.react-server.mjs
+++ b/packages/react-server/lib/loader/node-loader.react-server.mjs
@@ -12,12 +12,23 @@ const reactClientUrl = pathToFileURL(alias["react/client"]);
const cwd = sys.cwd();
let options, outDir;
+const resolveCache = new Map();
export async function initialize(data) {
options = data?.options || {};
outDir = options.outDir || ".react-server";
}
export async function resolve(specifier, context, nextResolve) {
+ const cacheKey = specifier + "\0" + (context.parentURL ?? "");
+ const cached = resolveCache.get(cacheKey);
+ if (cached) return { ...cached, shortCircuit: true };
+
+ const result = await resolveUncached(specifier, context, nextResolve);
+ resolveCache.set(cacheKey, result);
+ return result;
+}
+
+async function resolveUncached(specifier, context, nextResolve) {
switch (specifier) {
case "@lazarv/react-server/dist/__react_server_config__/prebuilt":
return nextResolve(
diff --git a/packages/react-server/lib/plugins/file-router/entrypoint.jsx b/packages/react-server/lib/plugins/file-router/entrypoint.jsx
index a28d3254..3aa2893b 100644
--- a/packages/react-server/lib/plugins/file-router/entrypoint.jsx
+++ b/packages/react-server/lib/plugins/file-router/entrypoint.jsx
@@ -13,6 +13,7 @@ import {
middlewares,
pages,
routes,
+ warmup$ as warmupModules$,
} from "@lazarv/react-server/file-router/manifest";
import { useMatch } from "@lazarv/react-server/router";
import { context$, getContext } from "@lazarv/react-server/server/context.mjs";
@@ -302,3 +303,5 @@ export default async function App() {
return ;
}
+
+export { warmupModules$ as warmup$ };
diff --git a/packages/react-server/lib/plugins/file-router/plugin.mjs b/packages/react-server/lib/plugins/file-router/plugin.mjs
index 7c6b134d..45cecfbb 100644
--- a/packages/react-server/lib/plugins/file-router/plugin.mjs
+++ b/packages/react-server/lib/plugins/file-router/plugin.mjs
@@ -911,45 +911,77 @@ export default function viteReactServerRouter(options = {}) {
},
load(id) {
if (id === "virtual:@lazarv/react-server/file-router/manifest") {
+ // Collect all unique import specifiers and generate cached import vars.
+ // Each dynamic import is called once and the module is reused on subsequent requests.
+ let importIndex = 0;
+ const importCacheMap = new Map();
+ function cachedImport(specifier) {
+ if (!importCacheMap.has(specifier)) {
+ importCacheMap.set(specifier, `__import_cache_${importIndex++}__`);
+ }
+ const varName = importCacheMap.get(specifier);
+ return `(${varName} ??= import("${specifier}"))`;
+ }
+
+ const middlewareEntries = manifest.middlewares
+ .map(
+ ([src, path]) =>
+ `["${path}", async () => { return ${cachedImport(src)}; }]`
+ )
+ .join(",\n");
+
+ const routeEntries = entry.api
+ .map(({ directory, filename, src }) => {
+ const normalized = filename
+ .replace(/^\+*/g, "")
+ .replace(/\.\.\./g, "_dot_dot_dot_")
+ .replace(/(\{)[^}]*(\})/g, (match) =>
+ match.replace(/\./g, "_dot_")
+ )
+ .split(".");
+ const [method, name, ext] = apiEndpointRegExp.test(filename)
+ ? normalized
+ : [
+ "*",
+ normalized[0] === "server" ? "" : normalized[0],
+ normalized[0] === "server"
+ ? ""
+ : normalized.slice(1).join("."),
+ ];
+ const path = `/${directory}/${ext ? name : ""}`
+ .replace(/\/+$/g, "")
+ .replace(/_dot_dot_dot_/g, "...")
+ .replace(/_dot_/g, ".")
+ .replace(/(\{)([^}]*)(\})/g, "$2")
+ .replace(/^\/+/, "/");
+ return `["${method}", "${path}", async () => {
+ return ${cachedImport(src)};
+ }]`;
+ })
+ .join(",\n");
+
+ const pageEntries = manifest.pages
+ .map(([src, path, outlet, type]) => {
+ const pageSpecifier =
+ (type === "page" && !outlet) || (type === "default" && outlet)
+ ? `__react_server_router_page__${path}::${src}::.jsx`
+ : src;
+ return `["${path}", "${type}", ${outlet ? `"${outlet}"` : "null"}, async () => ${cachedImport(pageSpecifier)}, "${src}", async () => ${cachedImport(src)}]`;
+ })
+ .join(",\n");
+
+ // Generate cache variable declarations
+ const cacheVarDecls = Array.from(importCacheMap.values())
+ .map((v) => `let ${v};`)
+ .join("\n");
+
const code = `
+ ${cacheVarDecls}
const middlewares = [
- ${manifest.middlewares
- .map(
- ([src, path]) =>
- `["${path}", async () => { return import("${src}"); }]`
- )
- .join(",\n")}
+ ${middlewareEntries}
];
const routes = [
- ${entry.api
- .map(({ directory, filename, src }) => {
- const normalized = filename
- .replace(/^\+*/g, "")
- .replace(/\.\.\./g, "_dot_dot_dot_")
- .replace(/(\{)[^}]*(\})/g, (match) =>
- match.replace(/\./g, "_dot_")
- )
- .split(".");
- const [method, name, ext] = apiEndpointRegExp.test(filename)
- ? normalized
- : [
- "*",
- normalized[0] === "server" ? "" : normalized[0],
- normalized[0] === "server"
- ? ""
- : normalized.slice(1).join("."),
- ];
- const path = `/${directory}/${ext ? name : ""}`
- .replace(/\/+$/g, "")
- .replace(/_dot_dot_dot_/g, "...")
- .replace(/_dot_/g, ".")
- .replace(/(\{)([^}]*)(\})/g, "$2")
- .replace(/^\/+/, "/");
- return `["${method}", "${path}", async () => {
- return import("${src}");
- }]`;
- })
- .join(",\n")}
+ ${routeEntries}
].toSorted(
([aMethod, aPath], [bMethod, bPath]) =>
(aMethod === "*") - (bMethod === "*") ||
@@ -957,20 +989,16 @@ export default function viteReactServerRouter(options = {}) {
aPath.localeCompare(bPath)
);
const pages = [
- ${manifest.pages
- .map(
- ([src, path, outlet, type]) =>
- `["${path}", "${type}", ${outlet ? `"${outlet}"` : "null"}, async () => import("${
- (type === "page" && !outlet) ||
- (type === "default" && outlet)
- ? `__react_server_router_page__${path}::${src}::.jsx`
- : src
- }"), "${src}", async () => import("${src}")]`
- )
- .join(",\n")}
+ ${pageEntries}
];
- export { middlewares, routes, pages };`;
+ function warmup$() {
+ return Promise.all([${Array.from(importCacheMap.keys())
+ .map((specifier) => `import("${specifier}")`)
+ .join(", ")}]);
+ }
+
+ export { middlewares, routes, pages, warmup$ };`;
return code;
} else if (id.startsWith("virtual:__react_server_router_page__")) {
let [path, src] = id
diff --git a/packages/react-server/lib/start/ssr-handler.mjs b/packages/react-server/lib/start/ssr-handler.mjs
index bcfef6cd..00f038ee 100644
--- a/packages/react-server/lib/start/ssr-handler.mjs
+++ b/packages/react-server/lib/start/ssr-handler.mjs
@@ -103,7 +103,7 @@ export default async function ssrHandler(root, options = {}) {
);
const [
{ render },
- { default: Component, init$: root_init$ },
+ { default: Component, init$: root_init$, warmup$: root_warmup$ },
{ default: GlobalErrorComponent },
{ default: ErrorBoundary },
{ clientReferenceMap },
@@ -122,6 +122,12 @@ export default async function ssrHandler(root, options = {}) {
import("@lazarv/react-server/dist/server/client-reference-map"),
import("../../cache/rsc.mjs"),
]);
+
+ // Warm up all route module imports in the background.
+ // This populates the ESM module cache and the loader resolve cache
+ // so the first request doesn't pay the cold-import penalty.
+ // Intentionally not awaited — runs in parallel with remaining server setup.
+ root_warmup$?.()?.catch?.(() => {});
const collectClientModules = getRuntime(COLLECT_CLIENT_MODULES);
const collectStylesheets = getRuntime(COLLECT_STYLESHEETS);
const clientModules = getRuntime(COLLECT_CLIENT_MODULES)?.(rootModule) ?? [];
diff --git a/packages/react-server/server/render-dom.mjs b/packages/react-server/server/render-dom.mjs
index 06ea431a..64dad47e 100644
--- a/packages/react-server/server/render-dom.mjs
+++ b/packages/react-server/server/render-dom.mjs
@@ -19,6 +19,115 @@ import { LINK_QUEUE, MODULE_CACHE } from "./symbols.mjs";
const streamMap = new Map();
const preludeMap = new Map();
+// ── Byte-level JS string escaping for inline injection
+for (let i = 0; i < 0x20; i++) {
+ if (!NEEDS_ESC[i]) _esc(i, `\\u${i.toString(16).padStart(4, "0")}`);
+}
+
+/**
+ * Escape raw UTF-8 bytes for embedding in a JS double-quoted string literal
+ * inside a ');
+
function detectSplitUTF8(chunk) {
const bytes = new Uint8Array(chunk);
let cutIndex = bytes.length;
@@ -253,6 +362,14 @@ export const createRenderer = ({
let forwardDone = false;
let forwardNext = null;
let splitBuffer = new Uint8Array(0);
+
+ // Per-render pre-encoded prefix for hydrated inline scripts.
+ // Computed once here (depends on `outlet`) to avoid repeated
+ // string concat + TextEncoder.encode() on every flight chunk.
+ const _hydratedScriptPrefix = toBytes(
+ ``
+ // ── HOT PATH ────────────────────────────────────
+ // Byte-level JS string escaping: escape the raw
+ // flight bytes directly, concatenate with
+ // pre-encoded prefix/suffix. No decode, no
+ // JSON.stringify, no re-encode.
+ const escaped = escapeJSStringBytes(value);
+ yield concatBytes(
+ _hydratedScriptPrefix,
+ escaped,
+ HYDRATED_SCRIPT_SUFFIX
);
- yield script;
} else {
+ // ── COLD PATH (pre-hydration / remote) ──────────
+ // Runs only for the first few flight chunks before
+ // the hydration script is emitted. Uses string
+ // decode + JSON.stringify since bootstrapScripts
+ // are accumulated as strings.
+ const payload = decoder.decode(value, {
+ stream: true,
+ });
+ const chunk = `self.__flightWriter__${outlet}__.write(self.__flightEncoder__${outlet}__.encode(${JSON.stringify(
+ payload
+ )}));`;
bootstrapScripts.push(chunk);
}
}
@@ -390,17 +523,18 @@ export const createRenderer = ({
if (value) {
contentLength += value.length;
force = value[value.length - 1] !== 0x3e;
- const chunk = decoder.decode(value);
+
+ // Byte-level checks — no decode needed
if (firstChunk) {
firstChunk = false;
- if (!chunk.includes("")) {
+ if (bytesEndWith(value, SUSPENSE_END_BYTES)) {
done = true;
}
}
@@ -418,7 +552,6 @@ export const createRenderer = ({
hydrationContainer = "document.body";
}
- // TODO: bootstrapScripts should be buffers instead of strings, fix script parts should be pre-encoded buffers then yield copy of those buffers
const script = encoder.encode(
`