Everything below is generated from Forme's CI on every commit — not hand-written, and it fails the build if it drifts from reality. It covers what CI actually proves: PDF/UA-1 and PDF/A conformance per document (via veraPDF), native-vs-WASM byte determinism, and test coverage. Failures and unsupported cases are included where they exist. It is not a browser-parity claim and not a coverage percentage.
Validated with veraPDF 1.30.2. The corpus is 5 shipped templates (invoice, receipt, report, shipping-label, letter) and 4 HTML fixtures (letterhead, dashed-borders, statement, zebra-invoice). Each PDF/A column is the archival-and-accessible render — the same file validated against the PDF/A profile and PDF/UA-1.
| Document | PDF/UA-1 | PDF/A-2b + UA-1 | PDF/A-2a + UA-1 | PDF/A-3b + UA-1 | PDF/A-3a + UA-1 | PDF/A-4 + UA-1 | PDF/A-4f + UA-1 |
|---|---|---|---|---|---|---|---|
| template/invoice | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| template/receipt | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| template/report | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| template/shipping-label | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| template/letter | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| html/letterhead | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| html/dashed-borders | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| html/statement | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| html/zebra-invoice | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| northmoor/invoice-standard | ✓ | — | — | — | — | — | — |
| northmoor/invoice-detailed | ✓ | — | — | — | — | — | — |
| northmoor/credit-note | ✓ | — | — | — | — | — | — |
| northmoor/receipt | ✓ | — | — | — | — | — | — |
| northmoor/quote | ✓ | — | — | — | — | — | — |
| northmoor/statement | ✓ | — | — | — | — | — | — |
| northmoor/purchase-order | ✓ | — | — | — | — | — | — |
| northmoor/remittance-advice | ✓ | — | — | — | — | — | — |
| northmoor/expense-report | ✓ | — | — | — | — | — | — |
| northmoor/payslip | ✓ | — | — | — | — | — | — |
| northmoor/letterhead | ✓ | — | — | — | — | — | — |
| northmoor/cover-letter | ✓ | — | — | — | — | — | — |
| northmoor/memo | ✓ | — | — | — | — | — | — |
| northmoor/employment-contract | ✓ | — | — | — | — | — | — |
| northmoor/nda | ✓ | — | — | — | — | — | — |
| northmoor/service-agreement | ✓ | — | — | — | — | — | — |
| northmoor/offer-letter | ✓ | — | — | — | — | — | — |
| northmoor/termination-letter | ✓ | — | — | — | — | — | — |
| northmoor/reference-letter | ✓ | — | — | — | — | — | — |
| northmoor/policy-acknowledgement | ✓ | — | — | — | — | — | — |
| northmoor/report-annual | ✓ | — | — | — | — | — | — |
| northmoor/report-monthly | ✓ | — | — | — | — | — | — |
| northmoor/lab-report | ✓ | — | — | — | — | — | — |
| northmoor/inspection-report | ✓ | — | — | — | — | — | — |
| northmoor/shipping-label | ✓ | — | — | — | — | — | — |
| northmoor/packing-slip | ✓ | — | — | — | — | — | — |
| northmoor/delivery-note | ✓ | — | — | — | — | — | — |
| northmoor/certificate | ✓ | — | — | — | — | — | — |
| northmoor/product-catalog | ✓ | — | — | — | — | — | — |
| northmoor/meeting-minutes | ✓ | — | — | — | — | — | — |
| unicode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| page-background | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Any failed profile shows its veraPDF clause list here; there are currently none. PDF/A-2u/3u are supported and reachable but not gated separately (the a-levels are the strictest of each part).
One render call produces the human-readable invoice PDF and embeds the caller-supplied EN 16931 CII XML as a conformant PDF/A-3 associated file (pdfa:3b + pdfUa + facturX{EN 16931} via @formepdf/core renderDocument). The XML is ZUGFeRD corpus EN16931_Einfach.cii.xml (FeRD, Apache-2.0). Forme validates the container, not the invoice semantics — the XML content is the caller's.
| Check | Result |
|---|---|
| veraPDF PDF/A-3b | ✓ |
| veraPDF PDF/UA-1 | ✓ |
| Mustang (ZUGFeRD/Factur-X reference validator) | ✓ |
The same document renders to byte-identical PDFs across the native binary and the WASM targets, over the four HTML fixtures. These are distinct strengths of evidence and shown as such: native, node, and web are compared byte-for-byte; the bundler target embeds a hash-identical WASM module (so it computes identically by construction) and is exercised by functional browser and workerd renders — it is not itself byte-diffed here.
| Comparison | Fixture | Bytes | Identical |
|---|---|---|---|
| node↔web | letterhead | 3,845 | ✓ |
| native↔node | letterhead | 3,845 | ✓ |
| node↔web | report | 5,546 | ✓ |
| native↔node | report | 5,546 | ✓ |
| node↔web | zebra-invoice | 3,612 | ✓ |
| native↔node | zebra-invoice | 3,612 | ✓ |
| node↔web | dashed-borders | 1,909 | ✓ |
| native↔node | dashed-borders | 1,909 | ✓ |
Boolean structural assertions (page count, break positions, table structure, valign) read from Chrome's rendering of these fixtures and asserted exactly. NOT a general browser-parity claim or a measured diff.
Hand-reduced repros distilled from the ironpress parity corpus (third-party test material, MIT). Not upstream copies.
Source: gastongouron/ironpress (MIT).
Passing tests per suite, parsed from an actual run. Stated as a fact, not a percentage.
| engine (lib + integration) | 682 passed | ✓ |
| html crate | 276 passed | ✓ |
| @formepdf/core | 103 passed | ✓ |
| @formepdf/react | 284 passed | ✓ |
| @formepdf/preact | 73 passed | ✓ |
| @formepdf/svelte | 74 passed | ✓ |
| @formepdf/vue | 29 passed | ✓ |
| @formepdf/renderer | 29 passed | ✓ |
| @formepdf/mcp | 78 passed | ✓ |
| @formepdf/resend | 5 passed | ✓ |
| @formepdf/tailwind | 65 passed | ✓ |
| @formepdf/vscode | 11 passed | ✓ |
| total | 1,709 passed | ✓ |
Process/isolate start through first PDF byte, including module instantiation. Steady state after warm-up; median and p95 over the published iteration count. Puppeteer reuses one browser across iterations (setContent + page.pdf each). All targets and Puppeteer render byte-identical HTML from the fixed corpus, so every column is the same document. Corpus is fixed, committed, and hash-checked (6 documents).
Two environments are published side by side and neither is cherry-picked: a dev machine (clean, quiet, single run) shows the engine on good hardware; CI ubuntu-latest (a shared, noisy runner) is the conservative number and the one that is regenerated on every push. Where a document ran on one but not the other, the gap is shown rather than the row hidden.
| Target | dev (clean) | CI (shared) |
|---|---|---|
| native binary | 12 ms | 6 ms |
| workerd isolate (edge est.) | ~66 ms | ~64 ms |
| web (browser) | 91 ms | 121 ms |
| node WASM | 111 ms | 236 ms |
| Puppeteer | 372 ms | 230 ms |
The ~7.1MB WASM module compiles and instantiates in 1-10ms — cold start is dominated by first-render JIT warmup, not module size: the module is large on disk and cheap to instantiate. Puppeteer here is on a warm machine; cold serverless is 3-10s, and on many consumption tiers Chrome cannot boot at all.
workerd caveat miniflare runs a workerd subprocess whose routing overhead the real edge does not pay; wall time OVERSTATES production cold start (true edge isolate spin-up ~5ms). Instantiate/render split is accurate. Real production cold start needs a deployment (out of scope).
Peak WASM linear memory per document (engine allocation — the same on any machine). This decides whether a document is renderable on Workers at all.
| Document | Pages | Peak WASM memory | Fits 128MB |
|---|---|---|---|
| receipt | 1 | 7 MB | ✓ |
| report-6p | 6 | 8 MB | ✓ |
| letterhead-paged | 5 | 10 MB | ✓ |
| compliance | 2 | 7 MB | ✓ |
| invoice-50p | 50 | 60 MB | ✓ |
| ledger-500p | 500 | 839 MB | ✗ no |
Practical guidance: comfortable under ~50 pages, approaching the ceiling near ~100 pages, use node or native beyond that. A margin, not a threshold — a slightly wider table shifts the number.
Local miniflare has generous memory and will run documents the real edge would OOM: the 500-page ledger completes locally but its 839MB peak would exceed the 128MB edge limit.
Median warm render (node target for Forme; Puppeteer reuses one browser). Both environments shown; the rows where Forme loses are here, in order, not reordered.
| Document | Pages | Forme dev | Pptr dev | Forme CI | Pptr CI | Forme PDF | Pptr PDF |
|---|---|---|---|---|---|---|---|
| receipt | 1 | 17 ms | 48 ms | 9 ms | 26 ms | 3 KB | 46 KB |
| report-6p | 6 | 21 ms | 54 ms | 13 ms | 30 ms | 8 KB | 79 KB |
| letterhead-paged | 5 | 27 ms | 52 ms | 19 ms | 33 ms | 9 KB | 66 KB |
| compliance | 2 | 19 ms | 50 ms | 23 ms | 29 ms | 5 KB | 66 KB |
| invoice-50p | 27 | 171 ms | 233 ms | 265 ms | 417 ms | 106 KB | 1946 KB |
| ledger-500p | 267 | 2.74 s | 2.79 s | 4.93 s | 4.97 s | 1039 KB | 19599 KB |
Forme is ~2× faster on small documents with ~15× smaller output, and — after the allocation fixes below — now edges a warm pooled Chrome on the large table-heavy documents too (50p and 500p), where it previously trailed ~2–4×. Iteration counts per cell are in the raw artifact.
Forme originally trailed Puppeteer ~3.8× on ledger-500p, decomposed as two independent linear factors: ~2× from the page-number second layout pass (ledger crosses the 2→3 digit-width boundary at page 100, so it re-lays-out to widen the page-count field), and ~1.9× from per-node allocation churn. That decomposition was directionally right and quantitatively wrong in the engine's favor: fixing the allocation churn (−74% allocations, −88% bytes allocated; benchmarks/harness/dhatprofile) dropped per-pass layout from ~7.3 to ~1.8 ms/page — a ~4× per-pass gain, not the estimated ~1.9×. It isolates cleanly because parse and serialize (both untouched) didn't move. That alone flipped both large docs — ledger 9997→2284 ms, invoice 534→147 ms — from behind Puppeteer to ahead (ledger 2284 vs 2665 ms, invoice 147 vs 232 ms; same M3 Max, Puppeteer as a stable control moved <2%). The ~2× sentinel factor is NOT eliminated: ledger still runs 2 passes — it is now masked by the throughput win, i.e. forme leads while doing twice Chrome's layout work. Removing that second pass on digit-change docs is the remaining known inefficiency (scoping it to just the running element proved unsound — see the sentinel tracked-fix note) and would roughly halve ledger's layout again.
The full layout tree is retained until serialize (~2 MB/page), so peak memory is linear in document size and freed at exit — not a leak. The allocation fixes did NOT move peak (ledger wasm ~890 MB before and after): they removed transient churn, not retained state — a throughput/allocator-pressure win, not a memory-ceiling one.
| Phase (native) | invoice-50p | ledger-500p |
|---|---|---|
| parse | 16 ms | 120 ms |
| layout | 93 ms | 1.74 s |
| serialize | 20 ms | 228 ms |
| layout before fix | 368 ms | 7.23 s |
| layout ms/page/pass | 1.86 | 1.74 |
Per-pass layout dropped from ~7.3 to ~1.8 ms/page (the allocation fix) and stays flat 50→500 pages — linear, no super-linear scaling. Parse and serialize (untouched) didn't move — the control that isolates the layout gain.
Tracked fixes (no dates promised):
The published ~26ms figure was measured on a document doing two layout passes; the honest single-pass number is ~21ms. Benchmarking our own engine surfaced an unconditional second layout pass — a document with no page-number placeholder was re-laying out the whole tree for nothing. We fixed it; here is the before and after on identical documents.
| Document | Pages | Before | After | Passes |
|---|---|---|---|---|
| receipt | 1 | 19 ms | 17 ms | 2→1 |
| report-6p | 6 | 27 ms | 21 ms | 2→1 |
| compliance | 2 | 24 ms | 20 ms | 2→1 |
| no-counter-100p | 100 | 1.98 s | 1.03 s | 2→1 |
| no-counter-300p | 300 | 5.89 s | 3.09 s | 2→1 |
A/B: pre-fix WASM rebuilt from HEAD~1 vs the fix, same machine/harness, node target, warm median.