Skip to content
Inkoxide

Search covers every page and heading of the docs. Use ↑ ↓ and ↵.

Benchmarks

Render times, how they were measured, and where the time goes.

Edit this page

React documents

@inkoxide/render on two documents, an invoice styled with Tailwind classes and a 43-page report, on the server and in the browser. The documents are in benchmarks/documents. Apple M4 Pro (14 cores), 24 GB, macOS.

On the server

Node 24.14; median of 20 renders after a warm-up (10 for the report), in one process.

DocumentPagesAll cores1 thread
Invoice11.5 ms1.2 ms
Report: 40 sections, 1,000 table rows4341 ms54 ms

The first render in a new process, including loading the library, takes 166 ms for the invoice and 207 ms for the report (median of 5 processes).

In the browser

@inkoxide/render/browser, the engine as WebAssembly in a Web Worker, on a page built with Vite and served from localhost. Median of 20 renders after a warm-up (10 for the report).

BrowserDocumentPagesRenderLongest main-thread task
Chromium 153Invoice12.3 msnone over 50 ms
Chromium 153Report4356 msnone over 50 ms
WebKit 26.6Invoice13.0 msnot reported
WebKit 26.6Report4347 msnot reported
  • Main thread. The engine renders in its worker: what stays on the main thread (React, building the document tree) takes no task over 50 ms.
  • First render. From the start of the page's navigation to the first invoice, the download of the library, its engine and its fonts included, takes 121 ms in Chromium and 116 ms in WebKit (median of 3 page loads). From localhost: on a real network, the download dominates.
  • Download, with brotli. 1.76 MB: 1.18 MB of WebAssembly, 0.26 MB of JavaScript and 0.32 MB of fonts (Inter regular and bold). The module is downloaded once, and cached. Hyphenation patterns (1 to 350 KB a language) and Noto Sans, the fallback font, are downloaded only by a document that needs them; these documents need neither.
  • Firefox was not measured: the Playwright build of Firefox does not start on this machine.

The documents and the harnesses are in benchmarks/:

pnpm install
scripts/build-node.sh
scripts/build-wasm.sh
pnpm --filter @inkoxide/benchmarks bench           # on the server
pnpm --filter @inkoxide/benchmarks bench:browser   # in the browser (pnpm exec playwright install first)

Engine

These numbers measure the engine alone, from a JSON document to PDF bytes, without React.

Headline. scripts/bench-headline.sh renders fixtures/docs/report-100p-realistic.json (98 pages, 40 tables, two fonts, a header, and a footer with page numbers) 10 times with the release CLI and prints the median:

MachineAll coresOne thread
4-vCPU Intel Xeon @ 2.8 GHz (cloud container)130 ms277 ms

Larger documents. inkoxide bench on the same container (release, fat LTO, font subsetting on):

DocumentOne thread4 threads
invoice: 1 page, median of 2002.9 ms3.0 ms
table-dump-500p: 435 pages, 20,000 rows, 27 MB of JSON2.59 s1.51 s
First render in a process: invoice with 2 fonts5–8 ms
Peak RSS: table-dump-500p213 MB209 MB

table-dump-500p is generated by scripts/fixtures/table_dump.py into target/generated/. Times vary by about ±10% between runs on a shared machine.

Micro-benchmarks. cargo bench -p inkoxide --bench render runs the criterion benchmarks in crates/core/benches/render.rs, one per fixture. render/svg-text renders an SVG with 15 texts: each render lays the text out with its fonts, and reuses the SVG converted with that layout from an earlier render. On an Apple M4 Pro it takes about 0.8 ms, against 0.6 ms for render/svg, a page of SVG images without text.

render/text-pretty sets three paragraphs of 87 words with textWrap: 'pretty', next to the same paragraphs first fit. With those three paragraphs first fit as well, the page renders in 0.58 ms against 0.66 ms: pretty costs about 25 µs per paragraph (Apple M4 Pro, median of criterion's samples).

Where the time goes

A single-thread profile of table-dump-500p. Wall times come from an instrumented build; instruction shares from valgrind --tool=callgrind on a 2,000-row version of the same table, counted inside render.

StageWall timeShare of instructionsNotes
Parse0.25–0.45 s18.7%serde_json over 27 MB and 100,000 styled nodes; MessagePack input is faster
Validate0.05–0.09 s~2%
Shape0.44–0.58 s26.5%rustybuzz on every distinct word; repeated words come from a cache
Layout and pagination0.31–0.45 s5.4%simple table cells skip Taffy entirely
Paint0.36–0.39 s~9%display lists for 100,000 cells and their borders
Content streams0.15–0.19 s~6%19 MB of PDF operators
Deflate (level 6)0.49–0.58 s26.6%19 MB in, 2.6 MB out; compressionLevel trades size for speed

On several threads, shaping, painting, content streams and compression run in parallel per page.

Synced from docs/BENCHMARKS.md in the inkoxide repository.