Markdown rendering
Converts markdown documents to HTML using marked, then compresses the output with Brotli. A straightforward transform pipeline — parse, render, compress — that shows how well workers handle chained operations.
How it works
Section titled “How it works”The host generates markdown documents. Both the host and worker paths parse the markdown, render HTML, then Brotli-compress the result. Compressed byte totals are compared for parity before the benchmark runs.
Three files:
run_markdown_to_html.ts— a small runnable example that compares host and worker outputbench_markdown_to_html.ts— the optional host-vs-worker benchmarkutils.ts— markdown rendering, compression tasks, and shared helpers
Install
Section titled “Install”deno add --npm jsr:@vixeny/knittingdeno add npm:marked npm:mitatanpx jsr add @vixeny/knittingnpm i marked mitata# pnpm 10.9+pnpm add jsr:@vixeny/knitting
# fallback (older pnpm)pnpm dlx jsr add @vixeny/knitting
pnpm add marked mitata# yarn 4.9+yarn add jsr:@vixeny/knitting
# fallback (older yarn)yarn dlx jsr add @vixeny/knitting
yarn add marked mitatabunx jsr add @vixeny/knittingbun add marked mitatabun src/run_markdown_to_html.ts --threads 2deno run -A src/run_markdown_to_html.ts --threads 2npx tsx src/run_markdown_to_html.ts --threads 2Example markdown in:
# Knitting markdown example
This example renders markdown on the host and in a worker.
## Checklist
- Parse markdown- Render HTML- Compare outputsHTML out:
<h1>Knitting markdown example</h1><p>This example renders markdown on the host and in a worker.</p><h2>Checklist</h2>...Optional benchmark
Section titled “Optional benchmark”bun src/bench_markdown_to_html.tsdeno run -A src/bench_markdown_to_html.tsnpx tsx src/bench_markdown_to_html.tsExpected output:
byte parity check: host=18,720 worker=18,720 OK match
benchmark avg (ns) min ... max (ns)host 32,100 29,400 ... 41,200knitting 17,800 15,600 ... 24,300import { createPool, isMain } from "@vixeny/knitting";import { markdownToHtml, markdownToHtmlHost } from "./utils.ts";
function intArg(name: string, fallback: number): number { const i = process.argv.indexOf(`--${name}`); if (i !== -1 && i + 1 < process.argv.length) { const value = Number(process.argv[i + 1]); if (Number.isFinite(value) && value > 0) return Math.floor(value); } return fallback;}
const THREADS = intArg("threads", 2);
const SAMPLE_MARKDOWN = [ "# Knitting markdown example", "", "This example renders markdown on the host and in a worker.", "", "## Checklist", "", "- Parse markdown", "- Render HTML", "- Compare outputs", "", "```ts", "const status = 'ready';", "```",].join("\n");
async function main() { const hostHtml = markdownToHtmlHost(SAMPLE_MARKDOWN); const pool = createPool({ threads: THREADS })({ markdownToHtml });
try { const workerHtml = await pool.call.markdownToHtml(SAMPLE_MARKDOWN);
console.log("Markdown -> HTML example"); console.log("threads :", THREADS); console.log("same html :", hostHtml === workerHtml); console.log("html length :", workerHtml.length); console.log("html preview :", workerHtml.slice(0, 120), "..."); } finally { pool.shutdown(); }}
if (isMain) { main().catch((error) => { console.error(error); process.exitCode = 1; });}import { createPool, isMain } from "@vixeny/knitting";import { bench, boxplot, run, summary } from "mitata";import { buildMarkdownDocs, markdownToHtmlCompressed, markdownToHtmlCompressedHost, sumChunkBytes,} from "./utils.ts";
const THREADS = 2;const DOCS = 2_000;
function runHost(markdowns: string[]): number { let compressedBytes = 0; for (let i = 0; i < markdowns.length; i++) { compressedBytes += markdownToHtmlCompressedHost(markdowns[i]!).byteLength; } return compressedBytes;}
async function runWorkers( callRender: (markdown: string) => Promise<Uint8Array>, markdowns: string[],): Promise<number> { const jobs: Promise<Uint8Array>[] = []; for (let i = 0; i < markdowns.length; i++) { jobs.push(callRender(markdowns[i]!)); }
const chunks = await Promise.all(jobs); return sumChunkBytes(chunks);}
async function main() { const markdowns = buildMarkdownDocs(DOCS); const pool = createPool({ threads: THREADS })({ markdownToHtmlCompressed }); let sink = 0;
try { await runWorkers( pool.call.markdownToHtmlCompressed, markdowns, );
console.log("Markdown -> HTML benchmark (mitata)"); console.log("workload: parse + render + brotli"); console.log("docs per iteration:", DOCS.toLocaleString()); console.log("threads:", THREADS);
boxplot(() => { summary(() => { bench(`host (${DOCS.toLocaleString()} docs)`, () => { sink = runHost(markdowns); });
bench( `knitting (${THREADS} thread(s), ${DOCS.toLocaleString()} docs)`, async () => { sink = await runWorkers( pool.call.markdownToHtmlCompressed, markdowns, ); }, ); }); });
await run(); console.log("last compressed bytes:", sink.toLocaleString()); } finally { pool.shutdown(); }}
if (isMain) { main().catch((error) => { console.error(error); process.exitCode = 1; });}import { task } from "@vixeny/knitting";import { marked } from "marked";import { brotliCompressSync } from "node:zlib";
marked.setOptions({ gfm: true,});
export function markdownToHtmlHost(markdown: string): string { return marked.parse(markdown) as string;}
export const markdownToHtml = task<string, string>({ f: markdownToHtmlHost,});
export function markdownToHtmlCompressedHost(markdown: string) { const html = markdownToHtmlHost(markdown); return brotliCompressSync(html);}
export const markdownToHtmlCompressed = task<string, Buffer>({ f: markdownToHtmlCompressedHost,});
export const TOPICS = [ "workers", "schema", "compression", "batching", "latency", "rendering", "validation", "throughput",];
function pick<T>(arr: T[], i: number): T { return arr[i % arr.length]!;}
export function makeMarkdown(i: number): string { const topicA = pick(TOPICS, i); const topicB = pick(TOPICS, i + 3); const topicC = pick(TOPICS, i + 5); const id = i.toString(36);
return [ `# Job ${id.toUpperCase()}`, "", `This page documents a ${topicA} pipeline for ${topicB}.`, "", "## Checklist", "", `- Parse input payload ${id}`, "- Validate required fields and defaults", `- Render output for ${topicC}`, "", "## Sample code", "", "```ts", `const jobId = \"${id}\";`, 'const status = "ready";', "```", "", `Generated at 2026-01-${String((i % 27) + 1).padStart(2, "0")}.`, ].join("\n");}
export function buildMarkdownDocs(count: number): string[] { const docs = new Array<string>(count); for (let i = 0; i < count; i++) docs[i] = makeMarkdown(i); return docs;}
export function sumChunkBytes( chunks: ArrayLike<{ byteLength: number }>,): number { let total = 0; for (let i = 0; i < chunks.length; i++) { total += chunks[i]!.byteLength; } return total;}A clean pipeline example
Section titled “A clean pipeline example”This is the simplest rendering example — no component tree, no JSX, just string in, string out. It’s a good reference if you want to understand the worker pattern without the React SSR complexity. The same approach works for any transform chain: parse input, process it, compress or encode the result.