Skip to content

Latest commit

 

History

History
178 lines (149 loc) · 9.38 KB

RESULTS.md

File metadata and controls

178 lines (149 loc) · 9.38 KB

PDF benchmark: WebFluent vs. Puppeteer (headless Chrome)

One invoice, designed twice to look the same — once as a WebFluent template (invoice.wf), once as the HTML and CSS a Puppeteer pipeline would print (chrome/template.mjs, chrome/invoice.css) — rendered from the same data (invoice.json, 46 line items, 4 Letter pages) with the same two font files (fonts/: Manrope and JetBrains Mono, both variable TrueType). WebFluent on the left, Chrome on the right:

Page 1, WebFluent left, Chrome right

Measured on 2026-10-06. Every number below comes from one run of just bench-pdf on the machine described; raw timings are in results.json.

Results

Install

Download On disk
wf 5.3.0 (wf-v5.3.0-x86_64-linux.tar.gz) 9.5 MiB 22.8 MiB
npm install puppeteer@25.12.0 (default: npm packages + Chrome for Testing + chrome-headless-shell 154.0.8037.57) 305.7 MiB 678.9 MiB
— of which Chrome for Testing 187.1 MiB 392.6 MiB
— of which chrome-headless-shell 114.9 MiB 261.4 MiB
— of which npm packages (puppeteer, qrcode and their dependencies) 3.7 MiB 24.9 MiB

One cold render (a new process: start → PDF written → exit)

10 runs each, interleaved, after one priming run each.

Median Range Peak memory (PSS) Peak memory (RSS)
wf render (CLI) 75.3 ms 73.0 ms – 514 ms 28.7 MiB 31.4 MiB
Puppeteer + Chrome (default headless) 581 ms 570 ms – 598 ms 424.1 MiB 1164.0 MiB
Puppeteer + chrome-headless-shell 395 ms 366 ms – 415 ms 273.3 MiB 649.2 MiB

100 warm renders (one long-lived process)

The template (WebFluent) or the browser (Puppeteer) is set up once — the "setup" column, not counted in the renders — then 5 unmeasured renders, then 100 measured ones, one after another.

Total Median p95 Setup Peak memory (PSS) Peak memory (RSS)
WebFluent Rust API (Template::render_pdf) 4.37 s 43.4 ms 47.4 ms 0.8 ms 27.4 MiB 29.8 MiB
Puppeteer + Chrome, one tab reused 9.00 s 88.2 ms 99.0 ms 199 ms 538.0 MiB 1281.6 MiB
Puppeteer + chrome-headless-shell, one tab reused 6.70 s 66.8 ms 70.5 ms 69.7 ms 427.5 MiB 808.0 MiB
Puppeteer + Chrome, a new tab per render 13.92 s 140 ms 148 ms 202 ms 758.9 MiB 2700.0 MiB

The documents

Size Pages Line items on each page
WebFluent 41 KiB 4 12 / 17 / 17 / 0
Chrome 360 KiB 4 12 / 17 / 17 / 0
chrome-headless-shell 355 KiB 4 12 / 17 / 17 / 0

Numbers you may quote

Each is rounded in Chrome's favour and set against the fastest or smallest Chrome configuration measured, not the default one. Say "on this invoice, on this machine".

  • Install: the wf Linux download is 9.5 MiB; a default npm install puppeteer downloads 305.7 MiB (12.1× more than wf even counting only its smaller browser, chrome-headless-shell).
  • Cold render: wf render produced the invoice PDF in a median 75.3 ms from process start, against 395 ms for a fresh Node process launching chrome-headless-shell through Puppeteer — 5.2× faster.
  • Warm renders: rendering the same invoice 100 times in-process took 4.37 s with WebFluent's Rust API (median 43.4 ms each) and 6.70 s with Puppeteer reusing one browser (median 66.8 ms) — 1.5× faster per render.
  • Memory: a cold wf render peaked at 28.7 MiB; Puppeteer and its browser processes together at 273.3 MiB (PSS). Warm, 27.4 MiB against 427.5 MiB.
  • Output: the same 4-page invoice is 41 KiB from WebFluent and 355 KiB from Chrome (see the caveat on fonts below).

Don't quote any ratio as true of PDFs in general: it is one document, and a different one would move it. Don't quote the cold numbers without "cold", or the warm ones without "warm".

Method

  • Same document. Both sides lay out the same data with the same fonts, the same colours, sizes and spacing; each is styled explicitly so neither leans on its engine's defaults. The WebFluent template uses Document, Footer, Watermark and QrCode; the HTML uses the CSS equivalents Chrome prints (@page margin boxes with counter(page)/counter(pages), a position: fixed watermark, and the qrcode npm package at the same error-correction level). The table header repeats on every page on both.
  • Same work per render. Each render starts from parsed data and ends with the PDF bytes in memory (cold: written to a file). WebFluent's render includes evaluating the template, reading and subsetting the fonts, layout and writing the PDF. Puppeteer's includes building the HTML string (template literal, number formatting, the QR code), page.setContent (waitUntil: "load") and page.pdf(); the fonts are inlined in the stylesheet as data URIs, read from disk once per process.
  • Cold is a fresh process each time (wf render … / node chrome/cold.mjs), timed from spawn to exit by the runner, after one priming run so every side's files are in the OS page cache. The very first launch of Chrome after a reboot or install is much slower than this (seconds) and is not measured.
  • Warm follows the usual advice for Puppeteer on a server: launch the browser once and keep it. Both tab strategies are measured. WebFluent's side is a small Rust program (rust/) that builds a Template once and calls render_pdf in a loop, with the release profile wf itself is built with.
  • Memory is measured in separate runs from the timings: the runner sums the RSS and the PSS of every process the command starts, every 10 ms, and keeps the peak (for a one-process command, GNU time's exact max RSS when it is higher). PSS splits a page several processes share among them, so Chrome's shared libraries count once; RSS counts them per process and overstates Chrome. Cold: the highest of three runs.
  • Sizes. wf: the GitHub release asset for the version measured, and its unpacked contents. Puppeteer: the Chrome for Testing archives it downloads, the packed npm tarballs its lock file names, and what all of it takes on disk afterwards.
  • The wf measured is built from this checkout (b3aa5f95) with cargo build --release, the same code the release binary is built from.

Machine

CPU Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz, 16 logical CPUs
RAM 30.6 GiB
OS Ubuntu 26.04.1 LTS, Linux 7.0.0-34-generic
Load average (1/5/15 min) before: 2.28 / 5.68 / 17.98; after: 2.75 / 4.84 / 16.42
wf 5.3.0 (b3aa5f95), rustc 1.99.0 (b940084d7 2026-09-28)
Node v22.23.0
Puppeteer 25.12.0
Chrome Chrome/154.0.8037.57 (Chrome for Testing), HeadlessChrome/154.0.8037.57 (chrome-headless-shell)

The machine was not otherwise idle: it is a desktop with a browser and editors open. The load average shows how busy it was; a quieter machine will give lower numbers on both sides.

Caveats

  • One document. An invoice: text, a long table, flex and grid boxes, borders, an SVG logo, a QR code. Documents heavy in pictures, or in CSS WebFluent's engine does not implement, would move the numbers, in either direction.
  • Chrome does more. It is a complete browser engine: every CSS feature, JavaScript on the page, web fonts from anywhere, canvas, and years of print-path fixes. WebFluent's paged engine implements the CSS its documents need (block, flex, grid, tables, the box model, the cascade) and is much younger: it has had far less real-world exposure than Chrome's print path.
  • Not pixel-identical. The two documents are as close as the two engines allow; see the picture above. They break pages in the same places (see "The documents") and wrap the same lines; small differences remain in the auto-sized table's column widths and a point or two of vertical spacing.
  • The file sizes are partly about fonts. Chrome writes these variable fonts as Type 3 fonts (outlines drawn per glyph, as pdffonts shows); WebFluent embeds them as subset TrueType (CID) fonts. In a one-off side test with static font files (Liberation, on the Chrome side only) Chrome's file came out at about 200 KiB: fonts are part of the difference, not all of it.
  • Puppeteer adds a layer. The Chrome numbers include Node and the DevTools protocol round trips, as every Puppeteer user's do. A pipeline that talks to Chrome directly, or keeps a pool of browsers, could do better; one that opens a new tab per render (also measured) does worse.
  • Parallelism is not measured. Every render here runs one at a time. A server rendering many documents at once would use several threads with WebFluent (a Template is Send + Sync) or several tabs or browsers with Puppeteer.
  • WebFluent re-reads its font files on every render (from the OS page cache), so its warm numbers include that work.

Reproduce

just bench-pdf

Needs Rust (stable), Node >=22 and npm, and network access for the first run (npm and Puppeteer's browser download, ~305.7 MiB). Optional: pdfinfo/pdftoppm (poppler) for the page counts and the picture, ImageMagick for the picture. Set BENCH_COLD_RUNS, BENCH_WARM_RENDERS or BENCH_WARMUP to change the counts.