The HTML-to-PDF problem

If you've ever generated PDFs in production, you know the pattern. You write an HTML template, style it with CSS, spin up a headless browser, call page.pdf(), and pray that the output matches what you saw in your browser's dev tools. Sometimes it does. Often it doesn't.

The root cause is a fundamental mismatch: HTML and CSS were designed for scrolling screens, not fixed-size pages. Every HTML-to-PDF tool is essentially a hack — a screen renderer forced to simulate a printer. This works well enough for simple cases, but the moment your documents need reliable pagination, precise measurements, or consistent output across environments, the cracks show.

The problems compound at scale. Headless Chrome consumes 200–400MB of RAM per instance. Rendering takes 2–5 seconds per document. CSS pagination rules (page-break-inside, orphans, widows) are implemented inconsistently across browser versions. A Chromium update can silently change your output. You're not generating documents — you're screenshotting a web page and hoping for the best.

What is Typst?

Typst is a compiled typesetting language created in 2023 by Martin Haug and Laurenz Mager, originally as a thesis project at TU Berlin. Think of it as a modern replacement for LaTeX: a language specifically designed to describe documents, not web pages.

Key characteristics that matter for PDF generation:

Typst vs LaTeX

Typst is often compared to LaTeX, but the developer experience is radically different. No package manager hell, no cryptic error messages, no 30-second compile times. Typst compiles incrementally in milliseconds and has clear, actionable error messages. If you've avoided LaTeX because of its complexity, Typst is worth a fresh look.

The 5 problems with HTML-to-PDF

1. Speed: headless browsers are slow

Generating a PDF with Puppeteer means launching (or connecting to) a Chromium instance, creating a new page, injecting HTML, waiting for network-idle, and calling the print-to-PDF function. Even with a warm browser pool, you're looking at 800ms–2,000ms per document. Cold starts push this to 4–5 seconds.

Typst compiles the same invoice in 50–200ms. There's no browser to launch, no DOM to construct, no layout engine to warm up. The compiler reads the source, runs the layout algorithm, and emits PDF bytes. This is a 10–20x improvement that directly impacts your API response times, your queue throughput, and your infrastructure costs.

Metric HTML-to-PDF (Puppeteer) Typst
Cold start 2,000–5,000ms 50–200ms
Warm render 800–2,000ms 50–200ms
Memory per render 200–400MB 15–30MB
Concurrent renders (4GB RAM) 10–15 100+
Binary size ~300MB (Chromium) ~15MB

2. CSS pagination is fundamentally broken

CSS was designed for continuous scroll, not discrete pages. The spec includes page-break-before, page-break-after, page-break-inside, orphans, and widows — but browser support is inconsistent and the behavior is often surprising.

Common failures in HTML-to-PDF pagination:

In Typst, pagination is a solved problem. The layout engine understands pages as a fundamental unit. Tables automatically repeat headers. Content flows across pages with predictable, controllable behavior. Repeating headers and footers are a single line of configuration, not a JavaScript workaround.

3. Memory consumption makes scaling expensive

Each Puppeteer instance runs a full Chromium process. In production, this means you need a browser pool, and each browser in that pool consumes 200–400MB of RAM — even when idle. A t3.medium instance (4GB RAM) can realistically handle 10–15 concurrent renders before you start swapping. You end up scaling vertically (bigger instances) instead of horizontally, which is both expensive and fragile.

Typst compiles documents in a single-threaded, stateless process that allocates ~20MB per render and releases it immediately. On the same t3.medium, you can handle 100+ concurrent renders. This is not a marginal improvement — it changes the economics of document generation entirely.

4. Reproducibility is not guaranteed

HTML-to-PDF output depends on the browser version, the operating system's font rendering stack, installed system fonts, and the specific combination of CSS features used. A Chromium update from 122 to 124 can change line heights, font metrics, or table layouts in ways that are invisible in a diff but visible on paper.

This is particularly painful for regulated industries (finance, healthcare, legal) where document output must be reproducible and auditable. "The PDF looks different because Chrome updated" is not an acceptable explanation when a regulator asks why your contract changed.

Typst embeds all fonts and uses its own layout engine. Same source, same output, every time. There is no external rendering dependency that can change between builds.

5. CSS is for screens, not paper

CSS lacks native concepts for things documents need: precise typographic control (kerning, ligatures, hanging punctuation), cross-references ("see page 5"), automatic table-of-contents generation, footnotes, margin notes, and running headers that change based on the current section. You can hack most of these with JavaScript and absolute positioning, but you're fighting the tool instead of using it.

Typst has all of these as built-in features. A footnote is #footnote[Your text here]. A cross-reference is @label. A running header is a function on the page template. These aren't libraries or plugins — they're part of the language.

How Typst solves each problem

Problem HTML-to-PDF approach Typst approach
Speed Browser pool, warm instances, queue workers Compiled in <200ms, no pool needed
Pagination CSS page-break rules (fragile) Native page-aware layout engine
Memory 200–400MB per Chromium instance ~20MB per compile, stateless
Reproducibility Depends on browser version + OS Deterministic, fonts embedded
Document features Hacked with JS + CSS tricks Footnotes, TOC, refs built-in
Headers/footers JS injection or absolute positioning First-class page template concept

Code comparison: the same invoice

Here's the same simple invoice generated with both approaches. Notice the difference in complexity and how each handles layout, pagination, and dynamic data.

invoice.html + Puppeteer
<!-- HTML template --> <html> <head><style> @page { size: A4; margin: 20mm; } body { font-family: Helvetica; } table { width: 100%; border-collapse: collapse; } th, td { padding: 8px 12px; border-bottom: 1px solid #eee; } .total { font-size: 24px; font-weight: bold; text-align: right; } /* Pagination? Good luck */ tr { page-break-inside: avoid; } </style></head> <body> <h1>Invoice {{number}}</h1> <p>To: {{client}}</p> <table> <tr><th>Item</th> <th>Qty</th> <th>Price</th></tr> {{#each items}} <tr><td>{{name}}</td> <td>{{qty}}</td> <td>{{price}}</td></tr> {{/each}} </table> <p class="total">{{total}}</p> </body></html> // Render: ~4200ms cold, ~800ms warm // RAM: ~250MB per browser instance
invoice.typ (Typst)
#set page(paper: "a4", margin: 20mm) #set text(font: "Helvetica", size: 10pt) // Header = Invoice {{ number }} To: {{ client }} #v(12pt) // Table with auto-repeating headers #table( columns: (1fr, auto, auto), stroke: none, table.header( [*Item*], [*Qty*], [*Price*] ), table.hline(stroke: 0.5pt), {% for item in items %} [{{ item.name }}], [{{ item.qty }}], [{{ item.price }}], {% endfor %} ) #v(12pt) #align(right)[ #text(size: 24pt, weight: "bold")[ {{ total }} ] ] // Render: ~120ms // RAM: ~20MB // Pagination: automatic // Table headers repeat on page 2+

The Typst version is shorter, more readable, and produces better output. But the real difference is what happens when the invoice has 200 line items and spans 8 pages. The HTML version will break rows across pages, lose table headers, and require JavaScript hacks to add page numbers. The Typst version handles all of this automatically.

When HTML-to-PDF still makes sense

Typst is not always the right answer. HTML-to-PDF tools have legitimate use cases:

Existing HTML content

If you're converting existing web pages, marketing emails, or HTML reports to PDF for archival purposes, using a browser-based tool is the path of least resistance. Re-authoring in Typst is unnecessary overhead.

JavaScript-dependent rendering

Documents that require JS-rendered charts (D3, Chart.js), interactive components captured as snapshots, or content loaded via AJAX need a real browser. Typst has no JavaScript runtime.

Large existing template libraries

If your team maintains 50+ HTML/CSS templates that work reliably, the migration cost to Typst may not justify the performance gains. Optimize incrementally — migrate high-volume templates first.

Web-to-PDF parity

When the PDF must look identical to a web page that users also view in a browser (e.g., a print stylesheet for a dashboard), browser-based rendering guarantees pixel-level parity.

Practical guidance

If your use case is "data in, document out" (invoices, contracts, reports, certificates, pay slips, receipts), Typst is the better tool. If your use case is "web page to PDF," a browser-based tool is the better tool. Most production PDF generation falls into the first category.

How Typsetter uses Typst

Typsetter is a hosted API built on Typst. We handle the infrastructure so you don't have to compile Typst yourself, manage servers, or think about concurrency. Here's how it works:

Typsetter API — generate a PDF
const response = await fetch('https://api.typsetter.dev/v1/render', { method: 'POST', headers: { 'Authorization': 'Bearer ts_live_sk_YOUR_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ template: 'invoice-professional', format: 'pdf', data: { invoice_number: 'INV-2026-0042', client_name: 'Acme Corp', items: [ { name: 'API Integration', qty: 1, price: '$3,200' }, { name: 'Monthly Support', qty: 3, price: '$450' }, ], total: '$4,550.00' } }) }); const pdfBytes = await response.arrayBuffer(); // ~340ms total, including network latency // No Chromium. No browser pool. No memory leaks.

The key difference between using Typsetter and running Typst yourself: you don't need to manage the Typst compiler, handle font loading, configure a compilation environment, or build the template injection layer. The API abstracts all of this behind a single HTTP call.

Conclusion: the right tool for the right job

HTML-to-PDF tools were built in an era when the browser was the only rendering engine capable of handling rich layouts. That era is over. Typst proves that a purpose-built document compiler can be faster, lighter, more reliable, and produce better typographic output than any browser-based approach.

The numbers speak for themselves:

If you're still running Puppeteer or wkhtmltopdf for data-driven documents — invoices, contracts, reports, certificates — you're paying a browser tax for capabilities you don't need. Typst eliminates that tax entirely.

Try Typst-powered PDF generation

100 PDFs/month free. No Chromium required. API key in 30 seconds.