SSR + compression
Takes the React SSR example one step further: after rendering HTML, compress it with Brotli. This tests whether it’s better to compress on the worker (render + compress in one shot) or on the host (render on worker, compress on main thread). Spoiler: doing both on the worker usually wins because you avoid sending uncompressed HTML back across the thread boundary.
How it works
Section titled “How it works”The host generates JSON payload strings. Both paths render the same user card component, then Brotli-compress the HTML output. The benchmark compares compressed byte totals for parity, then measures throughput.
bench_react_ssr_compress.ts— the benchmarkrender_user_card_compressed.tsx— the SSR + compression taskutils.ts— shared payload and compression helpers
Example input and output
Section titled “Example input and output”Input is the same JSON payload shape as the plain React SSR example. The worker call is:
const compressed = await pool.call.renderUserCardCompressed(payloadJson);Output is a Brotli-compressed Buffer, not an HTML string. That is the point of this example:
do the expensive render and compression work in one place, then return the smaller payload.
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 mitataCompression uses built-in node:zlib — no extra packages.
Optional benchmark
Section titled “Optional benchmark”bun src/bench_react_ssr_compress.tsdeno run -A src/bench_react_ssr_compress.tsnpx tsx src/bench_react_ssr_compress.tsExpected output:
byte parity check: host=42,380 worker=42,380 OK match
benchmark avg (ns) min ... max (ns)host 45,600 41,200 ... 58,300knitting 24,100 21,800 ... 32,400Brotli is significantly more expensive than renderToString alone, so the worker advantage is more pronounced here than in the plain SSR example.
import { task } from "@vixeny/knitting";import { brotliCompressSync } from "node:zlib";import { renderUserCardHost } from "../react_ssr/render_user_card.tsx";
function compressHtml(html: string) { return brotliCompressSync(html);}
export const renderUserCardCompressed = task({ f: (payload: string) => { const html = renderUserCardHost(payload); const compressed = compressHtml(html); return compressed; },});import { createPool, isMain } from "@vixeny/knitting";import { bench, boxplot, run, summary } from "mitata";import { renderUserCardHost } from "../react_ssr/render_user_card.tsx";import { renderUserCardCompressed } from "./render_user_card_compressed.tsx";import { buildCompressionPayloads, compressHtml, sumCompressedBytes,} from "./utils.ts";
const THREADS = 1;const REQUESTS = 100;
async function main() { const payloads = buildCompressionPayloads(REQUESTS); const pool = createPool({ threads: THREADS, inliner: { batchSize: 8, }, })({ renderUserCardCompressed }); let sink = 0;
try { runHost(payloads); await runWorkers( pool.call.renderUserCardCompressed, payloads, );
console.log("React SSR + compression benchmark (mitata)"); console.log("workload: parse + normalize + render + brotli"); console.log("requests per iteration:", REQUESTS.toLocaleString()); console.log("threads:", THREADS, " + main");
boxplot(() => { summary(() => { bench(`host (${REQUESTS.toLocaleString()} req)`, () => { sink = runHost(payloads); });
bench( `knitting (${THREADS} thread(s), ${REQUESTS.toLocaleString()} req)`, async () => { sink = await runWorkers( pool.call.renderUserCardCompressed, payloads, ); }, ); }); });
await run(); console.log("last compressed bytes:", sink.toLocaleString()); } finally { pool.shutdown(); }}
function runHost(payloads: string[]): number { let compressedBytes = 0; for (let i = 0; i < payloads.length; i++) { const html = renderUserCardHost(payloads[i]!); compressedBytes += compressHtml(html).byteLength; } return compressedBytes;}
async function runWorkers( callRender: (payload: string) => Promise<{ byteLength: number }>, payloads: string[],): Promise<number> { const jobs: Promise<{ byteLength: number }>[] = []; for (let i = 0; i < payloads.length; i++) { jobs.push(callRender(payloads[i]!)); }
const results = await Promise.all(jobs); return sumCompressedBytes(results);}
if (isMain) { main().catch((error) => { console.error(error); process.exitCode = 1; });}import { brotliCompressSync } from "node:zlib";import { renderUserCardHost } from "../react_ssr/render_user_card.tsx";import { buildUserPayloads } from "../react_ssr/utils.ts";
export type CompressionResult = { ms: number; bytes: number;};
export function buildCompressionPayloads(count: number): string[] { return buildUserPayloads(count);}
export function compressHtml(html: string) { return brotliCompressSync(html);}
export function sumCompressedBytes( chunks: ArrayLike<{ byteLength: number }>,): number { let total = 0; for (let i = 0; i < chunks.length; i++) { total += chunks[i]!.byteLength; } return total;}
export function runHostCompression(payloads: string[]): CompressionResult { const started = performance.now(); let compressedBytes = 0;
for (let i = 0; i < payloads.length; i++) { const html = renderUserCardHost(payloads[i]!); compressedBytes += compressHtml(html).byteLength; }
return { ms: performance.now() - started, bytes: compressedBytes };}
export function printCompressionMetrics( mode: string, requests: number, ms: number, compressedBytes: number,): void { const secs = Math.max(1e-9, ms / 1000); const rps = requests / secs; console.log(`${mode} took : ${ms.toFixed(2)} ms`); console.log(`${mode} throughput : ${rps.toFixed(0)} req/s`); console.log(`${mode} compressed : ${compressedBytes.toLocaleString()}`);}Compression placement matters
Section titled “Compression placement matters”Where you compress affects both throughput and data transfer. If you compress on the worker, you send a small compressed buffer back to the host. If you compress on the host, you first transfer the full uncompressed HTML string across the thread boundary, then compress it. For response compression in a real server, doing render + compress on the worker is almost always the right call.