Playwright and Puppeteer both generate PDFs through the same underlying Chrome DevTools Protocol, so choosing between them is less about output quality and more about developer experience, maintenance, and production operability. This guide covers the API differences, performance, deployment constraints, and when neither library is the right tool.
Quick comparison
| Factor | Playwright | Puppeteer | PDF REST API |
|---|---|---|---|
| Maintained by | Microsoft | Google (Chrome DevTools) | SaaS provider |
| Browser support | Chromium, Firefox, WebKit | Chromium only | Chromium (hosted) |
| TypeScript types | Built-in, complete | External (@types/puppeteer) | Not applicable |
| page.pdf() API | ✅ Same CDP params | ✅ Identical output | ✅ HTTP endpoint |
| Serverless support | ❌ Complex (~300MB binary) | ❌ Complex (~300MB binary) | ✅ Just HTTP |
| Docker image size | +700–900MB | +600–800MB | +0MB |
| Concurrent renders | Manual (page pool needed) | Manual (page pool needed) | Handled by API |
| Cold start | 200–500ms (warm) | 200–500ms (warm) | ~200ms |
| CSS Grid/Flexbox | Full (Chromium) | Full (Chromium) | Full (Chromium) |
What is Playwright PDF generation?
Playwright is an end-to-end testing and browser automation library maintained by Microsoft. Its page.pdf() method wraps the Chrome DevTools Protocol Page.printToPDF command to produce a PDF from a rendered page. Playwright supports Chromium, Firefox, and WebKit, but PDF generation only works with Chromium.
The core API is:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent('<h1>Hello PDF</h1>', { waitUntil: 'load' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
await browser.close();
// pdf is a BufferPlaywright's page.pdf() accepts the same parameters as the Chrome DevTools Protocol: format, width, height, margin, scale, landscape, printBackground, displayHeaderFooter, headerTemplate, footerTemplate, and pageRanges.
What is Puppeteer PDF generation?
Puppeteer is a Node.js library maintained by the Chrome DevTools team at Google. It predates Playwright and has a larger body of existing tutorials and Stack Overflow answers. Its page.pdf() method uses the same Chrome DevTools Protocol as Playwright, so PDF output is identical when both target Chromium.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent('<h1>Hello PDF</h1>', { waitUntil: 'load' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
await browser.close();
// pdf is a BufferThe APIs are nearly identical. If you have Puppeteer code, migrating to Playwright is mostly a find-and-replace of import paths and browser launch calls.
Playwright vs Puppeteer: practical differences
API design and TypeScript support
Playwright ships with first-party TypeScript definitions and does not require a separate @types/ package. All options are typed, including page.pdf() parameters. Puppeteer's TypeScript support improved in v19+ but historically required @types/puppeteer.
For new projects in 2026, Playwright's TypeScript integration is cleaner.
Browser coverage
Puppeteer is Chromium-only. Playwright supports three browser engines: Chromium, Firefox (via Playwright's patched fork), and WebKit. For PDF generation specifically, this difference does not matter because page.pdf() is only available on Chromium in both libraries.
Maintenance cadence
Playwright ships new versions approximately every two weeks as of early 2026. Puppeteer releases are less frequent. Both are actively maintained, but Playwright has the faster iteration cycle for bug fixes.
Header and footer templates
Both libraries support displayHeaderFooter with headerTemplate and footerTemplate as HTML strings. The templates load in an isolated rendering context and do not inherit page styles or JavaScript. To use a custom font in a header, you must embed it as a base64 data URI in the template itself.
Playwright's documentation for header/footer options is more complete and includes working examples. Puppeteer's header/footer behavior has historically been inconsistent with some Chrome versions.
How to generate PDFs with Playwright (full example)
This example generates a PDF invoice from an HTML template in a Node.js application:
import { chromium, Browser, BrowserContext } from 'playwright';
// Singleton browser: reuse across requests to avoid cold starts
let browser: Browser | null = null;
async function getBrowser(): Promise<Browser> {
if (!browser || !browser.isConnected()) {
browser = await chromium.launch({ headless: true });
}
return browser;
}
export async function generatePdf(html: string): Promise<Buffer> {
const b = await getBrowser();
const page = await b.newPage();
try {
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
return pdf;
} finally {
await page.close();
}
}The singleton browser pattern is important: launching a new browser for every PDF request adds 200–400ms and wastes memory. Reusing a single browser instance and closing only the page keeps cold starts under 10ms after warmup.
How to generate PDFs with Puppeteer (full example)
The Puppeteer equivalent is almost identical in structure:
import puppeteer, { Browser } from 'puppeteer';
let browser: Browser | null = null;
async function getBrowser(): Promise<Browser> {
if (!browser || !browser.isConnected()) {
browser = await puppeteer.launch({ headless: true });
}
return browser;
}
export async function generatePdf(html: string): Promise<Buffer> {
const b = await getBrowser();
const page = await b.newPage();
try {
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
return pdf;
} finally {
await page.close();
}
}Note the small API difference: Playwright uses waitUntil: 'networkidle' while Puppeteer uses waitUntil: 'networkidle0'. Both wait for the network to be idle for 500ms, which is necessary to ensure external fonts and images have loaded before generating the PDF.
Performance benchmarks
Both libraries wrap the same Chrome DevTools Protocol, so render time for a given HTML document is identical. The measurable differences are in startup overhead and concurrency.
| Operation | Playwright | Puppeteer |
|---|---|---|
| Cold browser launch | 400–600ms | 350–550ms |
| First PDF (cold browser) | 600–1000ms | 550–900ms |
| Subsequent PDFs (warm browser, singleton) | 50–200ms | 50–200ms |
| Concurrent renders (without page pool) | ❌ Serialized | ❌ Serialized |
| Peak memory (Chromium + Node.js) | 200–400MB | 200–400MB |
These benchmarks are for simple A4 HTML documents on a 2-core Linux VM. Complex layouts with external CSS and images will be slower. See the HTML-to-PDF benchmark for more detailed performance data.
The key finding: with a warm browser pool, both Playwright and Puppeteer can render a PDF in 50–200ms. Without a pool, every request pays the 400–600ms browser launch penalty.
Production problems with Playwright and Puppeteer
Both libraries share the same production pain points because they both run a headless Chromium instance.
Docker image size
A Node.js application with Playwright or Puppeteer needs Chromium and its system dependencies (fonts, libX11, libXcb, libXcomposite, and others). The resulting Docker image is 700MB–1GB depending on the base image. This slows CI pipelines and increases container registry costs.
The official Playwright Docker base image (mcr.microsoft.com/playwright:v1.41.0-focal) is 830MB before adding application code.
Serverless environments
AWS Lambda has a 250MB deployment package limit for zip uploads (10GB for container images, but cold starts increase). Vercel Edge Functions have a 50MB limit. Neither accommodates a 300MB Chromium binary without workarounds.
Common serverless workarounds include:
- @sparticuz/chromium: a prebuilt Chromium for Lambda (~45MB compressed via Brotli)
- Lambda Layers with a custom Chromium build
- Container-based Lambda functions (bypasses the 250MB limit)
Each workaround adds significant complexity and a maintenance burden for every Chromium update.
Concurrency
A single Chromium browser can run multiple pages in parallel, but each page.pdf() call is CPU and memory intensive. Without a page pool, concurrent requests serialize through one browser instance. A production page pool requires custom code: tracking available pages, queuing requests, handling page crashes, and recycling pages after a set number of renders.
This is not a small amount of code. A robust pool implementation with crash recovery is 100–200 lines of TypeScript before testing.
Browser crashes
Chromium crashes under memory pressure, on malformed HTML, or after rendering a large number of documents without recycling. A production Playwright/Puppeteer service needs:
- Process crash detection and browser restart
- Request timeout handling (PDF generation can stall on malformed HTML)
- Graceful shutdown to avoid orphaned browser processes
- Health check endpoints for container orchestrators
These requirements are manageable but represent ongoing operational work.
When to use a PDF API instead
A managed PDF API like PDF4.dev replaces the Playwright/Puppeteer infrastructure with an HTTP request. The same Chromium engine runs server-side, operated by the API provider.
The code reduction is significant:
DIY Playwright (30+ lines):
import { chromium, Browser } from 'playwright';
let browser: Browser | null = null;
async function getBrowser() {
if (!browser || !browser.isConnected()) {
browser = await chromium.launch({ headless: true });
}
return browser;
}
export async function generatePdf(html: string): Promise<Buffer> {
const b = await getBrowser();
const page = await b.newPage();
try {
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
return pdf;
} finally {
await page.close();
}
}PDF4.dev API (5 lines):
const res = await fetch('https://pdf4.dev/api/v1/render', {
method: 'POST',
headers: { 'Authorization': 'Bearer p4_live_xxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ html, format: { preset: 'a4' } }),
});
const pdf = await res.arrayBuffer();PDF4.dev runs the same Chromium engine as Playwright and Puppeteer. The output is identical, but you skip browser management, Docker bloat, and concurrency code. Try it free with no credit card required.
The decision comes down to infrastructure ownership:
| Scenario | Recommendation |
|---|---|
| Prototype, low volume (under 100 PDFs/day) | Playwright or Puppeteer |
| Serverless (Lambda, Vercel, Cloudflare) | PDF API |
| Docker image size matters | PDF API |
| Need concurrency without custom pooling | PDF API |
| Production with on-call obligation | PDF API |
| Air-gapped environment (no external HTTP) | Playwright or Puppeteer |
| Custom Chromium flags or extensions | Playwright or Puppeteer |
Playwright vs Puppeteer: which should you pick?
If you are starting a new project and want a DIY approach, Playwright is the better choice in 2026. It has better TypeScript types, a faster release cycle, and more complete documentation for PDF-specific options like header/footer templates.
Puppeteer is a reasonable choice if your team has existing experience with it or if you rely on Puppeteer-specific plugins from the npm ecosystem. The PDF output is identical.
Both share the same operational costs in production: Docker image size, serverless deployment complexity, and concurrency management. If those costs matter for your project, a managed PDF API removes them entirely.
For more detail on how Chromium-based PDF generation works under the hood, see the complete HTML-to-PDF benchmark and the Node.js PDF generation guide. For CSS layout tips that apply to both libraries, see the CSS print styles guide.
You can also test HTML-to-PDF rendering directly in the browser with the free HTML to PDF converter on PDF4.dev.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.