Log In Sign Up

Blog

HTML to PDF: the complete guide

Every approach to converting HTML into PDF belongs to one of two families, and knowing which family a tool is in predicts almost everything about how it will behave.

Family one reimplements the browser. Libraries like dompdf, mpdf, TCPDF, WeasyPrint, xhtml2pdf, and jsPDF parse HTML and CSS themselves and draw a PDF directly. No browser involved: fast, light, easy to deploy. The catch is that they only speak the subset of CSS their maintainers have implemented, and browsers move faster than any reimplementation can.

Family two runs the browser. Headless Chromium (driven by Puppeteer, Playwright, or a rendering API) loads your page exactly as Chrome would, executes the JavaScript, and prints the result. Output is pixel-identical to the browser; the cost is that now someone is operating a browser fleet.

That’s the whole decision, and the rest of this guide is evidence. Unlike most write-ups of this topic, everything below was tested by us with the same two documents, and every output PDF is downloadable so you can check our claims.

The test, and the scoreboard

Two documents, used across our whole series: a modern invoice (Flexbox header, CSS Grid line items, woff2 web font, inline SVG) and a Chart.js operations report (stat tiles, two canvas charts, a table). Rendered through everything:

RendererFamilyLanguageTimeModern CSSJS / chartsFull write-up
dompdf 3.1.6ReimplementationPHP0.31sCollapsedNoPHP round
mpdf 8.3.1ReimplementationPHP0.39sCollapsed, SVG OKNoPHP round
TCPDF 6.11.3ReimplementationPHP0.09sUnstyled; deprecatedNoPHP round
WeasyPrint 69ReimplementationPython1.06sNear browser-parityNoPython round
xhtml2pdf 0.2.17ReimplementationPython0.23sCollapsedNoPython round
jsPDF 4.2.1ReimplementationNode/browsern/aNo server HTML at allBrowser-onlyNode round
wkhtmltopdf 0.12.62015 WebKitCLI2.9sFlexbox yes, Grid no2015-era engine, modern JS failsMigration guide
Puppeteer + ChromiumBrowserNode5.7s coldPerfectYesNode round
Capture APIBrowserAny (REST)one POSTPerfectYes (wait_network)API render, verified

Three results worth pulling out of the table:

  1. One reimplementation library has genuinely caught up on CSS: WeasyPrint. Its version 69 rendered our Flexbox-and-Grid invoice almost indistinguishably from Chrome, the only non-browser renderer to pass. If your documents are static and your stack speaks Python, it deserves real consideration.
  2. No reimplementation library executes JavaScript. Not one. Canvas charts render as empty boxes in every library we tested, including WeasyPrint, because there is no JS engine inside. Dashboards, reports with charts, and client-side-framework pages structurally require a browser.
  3. wkhtmltopdf is in neither useful camp anymore. It’s an archived project wrapping a 2015 browser: modern CSS partially works, modern JavaScript doesn’t parse, and no one is patching it. We wrote a dedicated migration guide with a flag-by-flag translation table.

Choosing by scenario

Simple, fully-controlled templates (invoices, receipts, labels). Write table-based, 2010-style HTML and a reimplementation library is genuinely the right answer: free, milliseconds per document, zero infrastructure. Our table-markup invoice through dompdf came out clean at 0.17s and 2 KB. The constraint is discipline: the template can never drift toward modern CSS, and nobody can hand you HTML you don’t control.

Modern CSS, still static. WeasyPrint if you’re in Python. In PHP and Node there is no equivalent; the libraries in those ecosystems remain years behind, so your options jump straight to a browser engine.

Anything with JavaScript, charts, or “must match the browser”. A browser engine is the only option, and your choice is operational: run it or rent it.

Running it means Puppeteer or Playwright driving headless Chromium: a ~130 MB binary, roughly 250 MB of memory per concurrent render, font packages, zombie-process management, a queue, and platform quirks like the Ubuntu AppArmor sandbox policy that crashed our first launch. Our production headless-Chrome guide covers the full checklist, including serverless options. At steady volume with engineering time available, this works well.

Renting it means a rendering API. Ours works like this, and the PDFs it produced for this series are downloadable next to the local Chromium references they match:

curl -u "$CONVERTERER_API_KEY:" https://api.converterer.com/jobs \
  -d url="https://yourapp.example/invoices/1024" \
  -d page_size=A4 \
  -d wait_network=true

Margins, headers and footers with page numbers, wait conditions for JavaScript-heavy pages, and HTTP auth for protected URLs are parameters on the same call (full reference). The free tier’s 100 conversions a month exists precisely so you can diff its output against your browser before believing us.

Getting the output right, whatever you choose

A few things bite everyone, in every family:

  • Print CSS is its own discipline. Page breaks (page-break-inside: avoid), @page margins, and print-media styles determine whether multi-page documents look designed or shredded. We keep a print CSS primer for exactly this.
  • Backgrounds default to off in browser engines. Puppeteer needs printBackground: true; our API prints them by default. The “where did my brand colour go” bug is this setting, every time.
  • Fonts need to be loaded before capture. Browser engines: wait for document.fonts.ready or use network-idle waits. Reimplementation libraries: check the formats they accept (dompdf wants TTF, not woff2; WeasyPrint handled woff2 fine in our test).
  • JavaScript needs explicit waiting. networkidle0 in Puppeteer, wait_network=true on our API. Without it you capture the page before your charts draw, and get exactly this.
  • Beyond ~50 paginated pages with book-grade typesetting needs (footnotes, cross-references, running headers), browsers stop being the right tool: that’s paged.js or commercial engines like PrinceXML territory, covered in the production guide.

The short version

If you remember one thing: find out which family a tool is in before you adopt it. Reimplementation libraries are excellent exactly as far as their CSS support extends and not one property further, and none of them run JavaScript. Browser engines render everything correctly and bill you in operations instead. WeasyPrint is the one library that has narrowed the CSS gap; nothing has narrowed the JavaScript one. When the operational bill isn’t worth paying, that’s what the API is for.