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.
How it works
Section titled “How it works”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 itselfrender_user_card.tsx— the SSR component and worker taskutils.ts— input payloads and normalization helpers
Example payload and result
Section titled “Example payload and result”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>Install
Section titled “Install”deno add --npm jsr:@vixeny/knittingdeno add npm:react npm:react-dom npm:mitatanpx jsr add @vixeny/knittingnpm i react react-dom mitata# pnpm 10.9+pnpm add jsr:@vixeny/knitting
# fallback (older pnpm)pnpm dlx jsr add @vixeny/knitting
pnpm add react react-dom mitata# yarn 4.9+yarn add jsr:@vixeny/knitting
# fallback (older yarn)yarn dlx jsr add @vixeny/knitting
yarn add react react-dom mitatabunx jsr add @vixeny/knittingbun add react react-dom mitataDeno setup (TSX + npm)
Section titled “Deno setup (TSX + npm)”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" }}Optional benchmark
Section titled “Optional benchmark”bun src/bench_react_ssr.tsdeno run -A src/bench_react_ssr.tsnpx tsx src/bench_react_ssr.tsExpected output:
byte parity check: host=284,160 worker=284,160 OK match
benchmark avg (ns) min ... max (ns)host 18,200 16,800 ... 24,500knitting 9,400 8,600 ... 14,100import { 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; });}import React from "react";import { renderToString } from "react-dom/server";import { task } from "@vixeny/knitting";import { clamp, engagementScore, formatJoinDate, initials, levelForScore, type NormalizedUser, normalizeUser,} from "./utils.ts";
function Stat({ label, value }: { label: string; value: number }) { return ( <div className="stat"> <span className="stat-value">{value.toLocaleString()}</span> <span className="stat-label">{label}</span> </div> );}
function Badge({ plan }: { plan: "free" | "pro" }) { const text = plan === "pro" ? "PRO" : "FREE"; return <span className={`badge badge-${plan}`}>{text}</span>;}
function UserCard({ user }: { user: NormalizedUser }) { const score = engagementScore(user.stats); const level = levelForScore(score); const joined = formatJoinDate(user.joinedAt); const profileCompleteness = (user.bio ? 30 : 0) + (user.location ? 20 : 0) + (user.tags.length ? 20 : 0) + (user.handle ? 10 : 0) + (user.stats.posts ? 20 : 0); const completeness = clamp(profileCompleteness, 10, 100); const topTags = user.tags.slice(0, 6); const achievementBadges = [ user.stats.followers >= 1000 ? "1k+ followers" : "", user.stats.likes >= 5000 ? "5k+ likes" : "", user.stats.posts >= 50 ? "50+ posts" : "", ].filter(Boolean);
return ( <article className="user-card" data-user-id={user.id}> <header className="user-header"> <div className="avatar" aria-hidden="true"> {initials(user.name)} </div> <div className="meta"> <div className="title-row"> <h2>{user.name}</h2> <Badge plan={user.plan} /> </div> <div className="handle">{user.handle}</div> <div className="subline"> <span>📍 {user.location}</span> <span>• Joined {joined}</span> </div> <div className="level"> <span className="pill">Level: {level}</span> <span className="pill subtle">Score {score.toLocaleString()}</span> </div> </div> <div className="alerts"> <span className="pill">{user.alerts.unread} unread</span> <span className="pill subtle"> Last login {user.alerts.lastLogin} </span> </div> </header>
<section className="bio"> <h3>Bio</h3> {user.bio ? <p>{user.bio}</p> : <p className="muted">No bio yet.</p>} </section>
<section className="profile-complete"> <h3>Profile completeness</h3> <div className="progress"> <div className="progress-bar" style={{ width: `${completeness}%` }} aria-label={`Profile completeness ${completeness}%`} /> </div> <div className="muted">{completeness}% complete</div> </section>
<section className="stats-grid" aria-label="User stats"> <Stat label="Posts" value={user.stats.posts} /> <Stat label="Followers" value={user.stats.followers} /> <Stat label="Following" value={user.stats.following} /> <Stat label="Likes" value={user.stats.likes} /> </section>
<section className="achievements"> <h3>Highlights</h3> <ul> {achievementBadges.length > 0 ? ( achievementBadges.map((badge) => ( <li key={badge} className="tag"> {badge} </li> )) ) : <li className="tag muted">Getting started</li>} </ul> </section>
<section className="tags"> <h3>Interests</h3> <ul> {topTags.length > 0 ? ( topTags.map((tag) => ( <li key={tag} className="tag"> {tag} </li> )) ) : <li className="tag muted">No tags</li>} </ul> </section>
<footer className="actions"> <button type="button">Follow</button> <button type="button">Message</button> <button type="button" className="ghost"> View profile </button> </footer> </article> );}
export function renderUserCardHost(payloadJson: string): string { const parsed = JSON.parse(payloadJson) as unknown; const user = normalizeUser(parsed); const html = renderToString(<UserCard user={user} />); return html;}
export const renderUserCard = task({ f: renderUserCardHost,});export type UserStats = { posts: number; followers: number; following: number; likes: number;};
export type UserAlerts = { unread: number; lastLogin: string;};
export type UserPayload = { id?: string; name?: string; handle?: string; bio?: string; plan?: "free" | "pro"; location?: string; joinedAt?: string; tags?: string[]; stats?: Partial<UserStats>; alerts?: Partial<UserAlerts>;};
export type NormalizedUser = Required<UserPayload> & { stats: UserStats; alerts: UserAlerts;};
const TAGS = [ "react", "ssr", "typescript", "performance", "parallel", "workers", "ui", "web",];const LOCATIONS = ["Austin, TX", "Seattle, WA", "Brooklyn, NY", "Denver, CO"];
function pickFrom<T>(arr: T[], index: number): T { return arr[index % arr.length]!;}
function toNumber(value: unknown, fallback: number): number { return Number.isFinite(value) ? Number(value) : fallback;}
function toStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.filter((item): item is string => typeof item === "string");}
export function makeUserPayloadJson(i: number): string { const short = i.toString(36);
return JSON.stringify({ id: `u${short}`, name: `User ${short.toUpperCase()}`, handle: `@${short}`, bio: `Building fast UIs. Coffee + TypeScript. (${short})`, plan: i % 7 === 0 ? "pro" : "free", location: pickFrom(LOCATIONS, i), joinedAt: `202${(i % 4) + 2}-0${(i % 8) + 1}-1${i % 9}`, tags: [ pickFrom(TAGS, i), pickFrom(TAGS, i + 1), pickFrom(TAGS, i + 2), pickFrom(TAGS, i + 3), ], stats: { posts: (i % 120) + 1, followers: (i * 13) % 50_000, following: (i * 7) % 5_000, likes: (i * 31) % 250_000, }, alerts: { unread: i % 25, lastLogin: `2026-0${(i % 8) + 1}-0${(i % 9) + 1}`, }, });}
export function buildUserPayloads(count: number): string[] { const payloads = new Array<string>(count); for (let i = 0; i < count; i++) payloads[i] = makeUserPayloadJson(i); return payloads;}
export function normalizeUser(payload: unknown): NormalizedUser { const obj = (payload ?? {}) as Record<string, unknown>;
const id = typeof obj.id === "string" ? obj.id : "unknown"; const name = typeof obj.name === "string" ? obj.name : "Anonymous"; const handle = typeof obj.handle === "string" ? obj.handle : `@${id}`; const bio = typeof obj.bio === "string" ? obj.bio : ""; const plan = obj.plan === "pro" ? "pro" : "free"; const location = typeof obj.location === "string" ? obj.location : "Unknown"; const joinedAt = typeof obj.joinedAt === "string" ? obj.joinedAt : "2024-05-01";
const statsRaw = (obj.stats ?? {}) as Record<string, unknown>; const alertsRaw = (obj.alerts ?? {}) as Record<string, unknown>;
const stats: UserStats = { posts: toNumber(statsRaw.posts, 0), followers: toNumber(statsRaw.followers, 0), following: toNumber(statsRaw.following, 0), likes: toNumber(statsRaw.likes, 0), };
const alerts: UserAlerts = { unread: toNumber(alertsRaw.unread, 0), lastLogin: typeof alertsRaw.lastLogin === "string" ? alertsRaw.lastLogin : "2026-01-18", };
const tags = toStringArray(obj.tags);
return { id, name, handle, bio, plan, location, joinedAt, tags, stats, alerts, };}
export function formatJoinDate(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleDateString("en-US", { month: "short", year: "numeric" });}
export function initials(name: string): string { const parts = name.trim().split(/\s+/); const first = parts[0]?.[0] ?? "U"; const second = parts.length > 1 ? parts[1]?.[0] ?? "" : ""; return (first + second).toUpperCase();}
export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value));}
export function engagementScore(stats: UserStats): number { return Math.round( stats.posts * 2 + stats.likes * 0.05 + stats.followers * 0.4 + stats.following * 0.1, );}
export function levelForScore(score: number): string { if (score >= 5000) return "Legend"; if (score >= 2500) return "Elite"; if (score >= 1000) return "Rising"; if (score >= 300) return "Active"; return "New";}Why SSR is a great fit for workers
Section titled “Why SSR is a great fit for workers”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.