Skip to content

React SSR

Renders React components to HTML strings on workers using renderToString. If your server does SSR, this is probably the example closest to your real workload — parse JSON input, normalize data, render a component, return HTML.

The host generates JSON payload strings. The host path calls renderUserCardHost directly (parse + normalize + renderToString). The worker path sends the same payloads through createPool. Byte totals are compared once to verify the host and worker produce identical output, then mitata benchmarks both paths.

Three files:

  • bench_react_ssr.ts — the benchmark itself
  • render_user_card.tsx — the SSR component and worker task
  • utils.ts — input payloads and normalization helpers

Input:

{
"id": "u42",
"name": "Ari Lane",
"handle": "@ari",
"bio": "Building fast UIs.",
"plan": "pro",
"location": "Austin, TX",
"joinedAt": "2026-01-18",
"tags": ["react", "ssr", "workers"],
"stats": { "posts": 42, "followers": 1200, "following": 180, "likes": 9800 },
"alerts": { "unread": 3, "lastLogin": "2026-01-18" }
}

Minimal usage:

const pool = createPool({ threads: 2 })({ renderUserCard });
const html = await pool.call.renderUserCard(payloadJson);

Result:

<article class="user-card" data-user-id="u42">...</article>
deno.sh
deno add --npm jsr:@vixeny/knitting
deno add npm:react npm:react-dom npm:mitata

If you run this with Deno and see Uncaught SyntaxError: Unexpected token '<', set a root deno.json so Deno transpiles TSX and resolves npm packages:

{
"nodeModulesDir": "auto",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
bun.sh
bun src/bench_react_ssr.ts

Expected output:

byte parity check: host=284,160 worker=284,160 OK match
benchmark avg (ns) min ... max (ns)
host 18,200 16,800 ... 24,500
knitting 9,400 8,600 ... 14,100
bench_react_ssr.ts
import { createPool, isMain } from "@vixeny/knitting";
import { bench, boxplot, run, summary } from "mitata";
import { renderUserCard, renderUserCardHost } from "./render_user_card.tsx";
import { buildUserPayloads } from "./utils.ts";
const THREADS = 1;
const REQUESTS = 2_000;
async function main() {
const payloads = buildUserPayloads(REQUESTS);
const pool = createPool({
threads: THREADS,
inliner: {
batchSize: 6,
},
})({ renderUserCard });
let sink = 0;
try {
const hostBytes = runHost(payloads);
const knittingBytes = await runWorkers(pool.call.renderUserCard, payloads);
if (hostBytes !== knittingBytes) {
throw new Error("Host and worker HTML byte totals differ.");
}
console.log("React SSR benchmark (mitata)");
console.log("workload: parse + normalize + render to HTML");
console.log("requests per iteration:", REQUESTS.toLocaleString());
console.log("threads:", THREADS, " + inliner");
boxplot(() => {
summary(() => {
bench(`host (${REQUESTS.toLocaleString()} req)`, () => {
sink = runHost(payloads);
});
bench(
`knitting (${THREADS} thread(s) + main , ${REQUESTS.toLocaleString()} req)`,
async () => {
sink = await runWorkers(pool.call.renderUserCard, payloads);
},
);
});
});
await run();
console.log("last html bytes:", sink.toLocaleString());
} finally {
pool.shutdown();
}
}
function runHost(payloads: string[]): number {
let htmlBytes = 0;
for (let i = 0; i < payloads.length; i++) {
const html = renderUserCardHost(payloads[i]!);
htmlBytes += html.length;
}
return htmlBytes;
}
async function runWorkers(
callRender: (payload: string) => Promise<string>,
payloads: string[],
): Promise<number> {
const jobs: Promise<string>[] = [];
for (let i = 0; i < payloads.length; i++) {
jobs.push(callRender(payloads[i]!));
}
const results = await Promise.all(jobs);
let htmlBytes = 0;
for (let i = 0; i < results.length; i++) {
htmlBytes += results[i]!.length;
}
return htmlBytes;
}
if (isMain) {
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}

renderToString is synchronous and CPU-bound — it walks your component tree and builds an HTML string. On a request-per-request basis it’s not usually slow, but under load it blocks the event loop and everything else waits. Moving SSR to a worker pool means your main thread stays free to accept requests and handle I/O while rendering happens in parallel. The Hono server example shows what this looks like in a real HTTP server context.