Back to Blog
Deep Dive

HTML to PDF in 2026: An Honest Comparison

2026-01-29·4 min read
comparisonpuppeteerpdf-libapi

Every PDF generation article ranks libraries without actually using them. This one's different.

I've shipped production code with all of these. Here's what I learned.

The Contenders

Library Approach Best For
pdf-lib Programmatic PDF creation Simple documents, forms
Puppeteer Headless Chrome rendering Complex layouts
Playwright Similar to Puppeteer Cross-browser needs
PDFKit Node.js PDF generation Streaming, server-side
PDF APIs External service Production at scale

pdf-lib

pdf-lib creates PDFs programmatically. No browser. No HTML.

typescript
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'

const doc = await PDFDocument.create()
const page = doc.addPage([595, 842]) // A4
const font = await doc.embedFont(StandardFonts.Helvetica)

page.drawText('Invoice #1234', {
  x: 50,
  y: 800,
  size: 24,
  font,
  color: rgb(0, 0, 0),
})

const pdfBytes = await doc.save()

Pros:

  • Tiny bundle (works in browser)
  • Fast (no browser rendering)
  • Can modify existing PDFs
  • No external dependencies

Cons:

  • Manual positioning for everything
  • No CSS. No HTML. No layout engine.
  • Complex documents become unmanageable
  • Text wrapping? You implement it.

Verdict: Great for filling forms or adding watermarks. Painful for invoices or reports.

PDFKit

PDFKit is Node.js native. Similar philosophy to pdf-lib.

typescript
import PDFDocument from 'pdfkit'
import fs from 'fs'

const doc = new PDFDocument()
doc.pipe(fs.createWriteStream('output.pdf'))

doc.fontSize(24).text('Invoice #1234', 50, 50)
doc.fontSize(12).text('Line item 1', 50, 100)

doc.end()

Pros:

  • Streaming output (memory efficient)
  • Good font support
  • Mature, stable

Cons:

  • Same manual positioning problem
  • Node.js only (no browser)
  • Layout is your problem

Verdict: Better than pdf-lib for server-side generation. Still painful for complex layouts.

Puppeteer / Playwright

Use a real browser to render HTML, then export to PDF.

typescript
import puppeteer from 'puppeteer'

const browser = await puppeteer.launch()
const page = await browser.newPage()

await page.setContent(`
  <html>
    <head>
      <style>
        body { font-family: sans-serif; }
        .invoice { padding: 40px; }
      </style>
    </head>
    <body>
      <div class="invoice">
        <h1>Invoice #1234</h1>
        <table>...</table>
      </div>
    </body>
  </html>
`)

const pdf = await page.pdf({ format: 'A4' })
await browser.close()

Pros:

  • Full CSS support (flexbox, grid, the works)
  • Use your existing HTML/CSS skills
  • Pixel-perfect rendering
  • Handles complex layouts easily

Cons:

  • Heavy (Chromium is 100MB+)
  • Slow (browser startup, rendering)
  • Memory hungry (200MB+ per instance)
  • Crashes in production (see my other post)

Verdict: Best layout engine. Worst operational characteristics.

Playwright vs Puppeteer

They're nearly identical for PDF generation.

typescript
// Playwright
import { chromium } from 'playwright'
const browser = await chromium.launch()

// Puppeteer
import puppeteer from 'puppeteer'
const browser = await puppeteer.launch()

Playwright supports Firefox and WebKit. Doesn't matter for PDFs—Chromium's PDF output is the best.

Verdict: Pick whichever you're already using. Don't switch.

wkhtmltopdf

The old guard. A Qt WebKit wrapper.

bash
wkhtmltopdf input.html output.pdf

Pros:

  • Simple CLI
  • Lightweight compared to Chromium

Cons:

  • Ancient WebKit (no modern CSS)
  • No flexbox, no grid, no CSS variables
  • Project is essentially abandoned
  • Security vulnerabilities

Verdict: Don't use it in 2026. The CSS support is stuck in 2015.

PDF APIs

External services that handle the rendering.

typescript
// Generic API call
const response = await fetch('https://api.pdfservice.com/generate', {
  method: 'POST',
  headers: { Authorization: 'Bearer xxx' },
  body: JSON.stringify({ html: '<h1>Invoice</h1>' }),
})

const pdf = await response.arrayBuffer()

Pros:

  • No Chromium to manage
  • Always warm (no cold starts)
  • Scales automatically
  • Someone else's problem

Cons:

  • Costs money
  • Data leaves your server
  • Latency (network round trip)
  • Vendor dependency

Verdict: Best for production. You're trading money for reliability.

The Decision Matrix

Requirement Best Choice
Fill existing PDF forms pdf-lib
Simple server-side docs PDFKit
Complex layouts, low volume Puppeteer
Complex layouts, production PDF API
Browser-side generation pdf-lib
Modify existing PDFs pdf-lib

My Honest Take

Here's what I actually use:

For prototypes: Puppeteer. It's the fastest to get working.

For production: An API. I'm not debugging Chromium crashes anymore.

For PDF manipulation: pdf-lib. Adding watermarks, merging files, filling forms.

The "right" answer depends on your scale. Under 100 PDFs/month? Puppeteer's fine. Over 1000? You'll spend more time on infrastructure than features.

About NeatPDF

Full disclosure: I built NeatPDF because I got tired of the Puppeteer dance.

It's a PDF API with a typed SDK. You send HTML, you get a PDF. No browser management.

typescript
import { NeatPDF } from '@neatpdf/sdk'

const client = new NeatPDF({ apiKey: process.env.NEATPDF_API_KEY })

const pdf = await client.generate({
  html: '<h1>Invoice #1234</h1>',
  format: 'A4',
  margin: { top: '20mm', bottom: '20mm' },
})

Zero-storage architecture. Your HTML goes in, PDF comes out, nothing saved.

Is it the right choice for everyone? No. If you're generating 10 PDFs a month, just use Puppeteer.

But if you're building a SaaS and PDF generation is a feature—not the product—an API makes sense.

Check the docs →

Ready to generate PDFs?

Zero data retention. GDPR compliant.

Start Free TrialZero data retention · GDPR