Playwright PDF Generation: page.pdf() Guide & API Migration
Playwright is widely used for browser automation and end-to-end testing — but its page.pdf() method also makes it a capable HTML-to-PDF converter. This guide covers everything from basic setup to production challenges, with complete code examples.
If you're migrating from Puppeteer, the approach is nearly identical — see the Puppeteer to PDF API Migration Guide for a side-by-side comparison.
Installation and Setup
Install Playwright
npm install playwright
# Install the Chromium browser (PDF generation is Chromium-only)
npx playwright install chromium
Basic PDF Generation
const { chromium } = require('playwright');
async function generatePdf(htmlContent) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Set HTML content and wait for fonts/images to load
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
});
await browser.close();
return pdfBuffer;
}
// Usage
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Inter, sans-serif; font-size: 14px; }
h1 { color: #1a56db; }
</style>
</head>
<body>
<h1>Invoice</h1>
<p>Generated with Playwright.</p>
</body>
</html>`;
generatePdf(html).then(buf => {
require('fs').writeFileSync('output.pdf', buf);
console.log('PDF generated');
});
Generate PDF from a URL
const { chromium } = require('playwright');
async function generatePdfFromUrl(url) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// Navigate and wait for all network requests to settle
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
});
await browser.close();
return pdfBuffer;
}
page.pdf() Options Reference
Paper Size and Orientation
const pdf = await page.pdf({
// Named format (takes priority over width/height)
format: 'A4', // A4, A3, Letter, Legal, Tabloid, etc.
// Custom dimensions (used only if format is not set)
width: '210mm',
height: '297mm',
// Landscape orientation
landscape: true,
// Rendering scale factor (0.1 to 2, default: 1)
scale: 1.0,
});
Margins
const pdf = await page.pdf({
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
});
Background Colors and Images
Browsers suppress CSS backgrounds in print by default. Enable printBackground to include them:
const pdf = await page.pdf({
printBackground: true, // Include CSS background-color, background-image
});
Headers and Footers
const pdf = await page.pdf({
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
<span class="title"></span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`,
// Ensure margins are large enough for header/footer
margin: { top: '40px', bottom: '40px', left: '20px', right: '20px' },
});
Available template classes:
| Class | Inserts |
|---|---|
pageNumber |
Current page number |
totalPages |
Total page count |
date |
Current date |
title |
Page <title> value |
url |
Page URL |
Tagged PDF and Outline (Playwright v1.42+)
const pdf = await page.pdf({
// Accessible tagged PDF (required for PDF/UA compliance)
tagged: true,
// Embed clickable bookmarks from heading structure
outline: true,
// Specific page range
pageRanges: '1-3, 5',
});
CSS Print Media Queries
Playwright applies @media print styles by default. Use print-specific CSS to control the PDF layout:
/* Screen layout */
.sidebar { display: block; width: 250px; }
.print-only { display: none; }
/* PDF / print layout */
@media print {
.sidebar { display: none; }
.print-only { display: block; }
/* Explicit page breaks */
.page-break {
break-before: page;
page-break-before: always; /* fallback */
}
/* Prevent breaks inside sections */
.invoice-section {
break-inside: avoid;
page-break-inside: avoid;
}
/* Keep headings with the following content */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid;
}
/* Prevent orphaned/widowed lines */
p {
orphans: 3;
widows: 3;
}
/* Paper size and margins */
@page {
size: A4;
margin: 20mm 15mm;
}
@page :first {
margin-top: 30mm; /* Extra top margin on first page */
}
}
Using Screen Media Instead of Print
If you want CSS @media screen styles to apply (not print styles):
await page.emulateMedia({ media: 'screen' });
const pdf = await page.pdf({ printBackground: true });
Complete Example: Japanese Invoice PDF
const { chromium } = require('playwright');
const invoiceData = {
invoiceNumber: 'INV-2026-0042',
issueDate: 'April 19, 2026',
dueDate: 'April 30, 2026',
clientName: 'Acme Corporation',
items: [
{ name: 'Web Application Development', qty: 1, unitPrice: 5000 },
{ name: 'UI/UX Design', qty: 1, unitPrice: 1500 },
],
};
function buildInvoiceHtml(data) {
const subtotal = data.items.reduce((s, i) => s + i.qty * i.unitPrice, 0);
const tax = Math.round(subtotal * 0.1);
const total = subtotal + tax;
const rows = data.items
.map(item => `
<tr>
<td>${item.name}</td>
<td style="text-align:right;">${item.qty}</td>
<td style="text-align:right;">$${item.unitPrice.toLocaleString()}</td>
<td style="text-align:right;">$${(item.qty * item.unitPrice).toLocaleString()}</td>
</tr>`)
.join('');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Inter, Arial, sans-serif; font-size: 13px; color: #1a1a1a; }
@page { size: A4; margin: 15mm 20mm; }
.page { max-width: 210mm; padding: 20px; }
.header {
display: flex; justify-content: space-between;
margin-bottom: 32px; border-bottom: 3px solid #1a56db; padding-bottom: 16px;
}
.title { font-size: 32px; font-weight: 700; color: #1a56db; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th { background: #1a56db; color: #fff; padding: 10px 12px; text-align: left; }
td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
td:last-child { text-align: right; }
.grand-total { font-size: 20px; font-weight: 700; color: #1a56db; }
</style>
</head>
<body>
<div class="page">
<div class="header">
<div class="title">INVOICE</div>
<div style="text-align:right; color:#6b7280; font-size:12px;">
<strong>${data.invoiceNumber}</strong><br>
Issued: ${data.issueDate}<br>
Due: ${data.dueDate}
</div>
</div>
<p><strong>Bill To: ${data.clientName}</strong></p>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Amount</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
<p style="text-align:right; margin-top:16px;">Subtotal: $${subtotal.toLocaleString()}</p>
<p style="text-align:right;">Tax (10%): $${tax.toLocaleString()}</p>
<p class="grand-total" style="text-align:right; margin-top:8px;">Total: $${total.toLocaleString()}</p>
</div>
</body>
</html>`;
}
async function main() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(buildInvoiceHtml(invoiceData), { waitUntil: 'networkidle' });
const buf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '15mm', bottom: '20mm', left: '20mm', right: '20mm' },
displayHeaderFooter: true,
footerTemplate: `
<div style="font-size:9px; width:100%; text-align:center; color:#9ca3af;">
Generated by Playwright — Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
});
require('fs').writeFileSync('invoice.pdf', buf);
await browser.close();
console.log('invoice.pdf created');
}
main();
TypeScript Version
import { chromium, Page } from 'playwright';
interface PdfOptions {
format?: 'A4' | 'A3' | 'Letter' | 'Legal';
landscape?: boolean;
printBackground?: boolean;
}
async function generatePdf(html: string, options: PdfOptions = {}): Promise<Buffer> {
const browser = await chromium.launch({ headless: true });
const page: Page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const pdfBuffer = await page.pdf({
format: options.format ?? 'A4',
landscape: options.landscape ?? false,
printBackground: options.printBackground ?? true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
await browser.close();
return pdfBuffer;
}
export { generatePdf };
Production Challenges
Playwright works well for low-volume or one-off PDF generation, but production workloads surface several challenges.
Memory Consumption
Each Chromium instance consumes 150–300MB of memory. Under concurrent load, memory usage scales linearly — leading to OOM crashes.
// Browser pool to limit concurrent Chromium instances
const { chromium } = require('playwright');
class BrowserPool {
constructor(maxSize = 3) {
this.maxSize = maxSize;
this.active = 0;
this.queue = [];
}
async acquire() {
if (this.active < this.maxSize) {
this.active++;
return chromium.launch({ headless: true });
}
return new Promise((resolve) => this.queue.push(resolve));
}
async release(browser) {
await browser.close();
this.active--;
if (this.queue.length > 0) {
const next = this.queue.shift();
this.active++;
const newBrowser = await chromium.launch({ headless: true });
next(newBrowser);
}
}
}
const pool = new BrowserPool(3);
async function generatePdfPooled(html) {
const browser = await pool.acquire();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const buf = await page.pdf({ format: 'A4', printBackground: true });
await page.close();
return buf;
} finally {
await pool.release(browser);
}
}
Cold Start Latency
Chromium takes 1–3 seconds to start. If you launch and close a browser per request, every PDF request will have that latency. The common workaround — keeping a browser alive — introduces memory leak risk over time.
Serverless Environments
| Environment | Max Memory | Cold Start | Chromium Support |
|---|---|---|---|
| AWS Lambda | 10 GB | 1–3 s | Requires @sparticuz/chromium |
| Google Cloud Run | 32 GB | ~100 ms | Works with standard Docker image |
| Vercel Functions | 3 GB | 1–2 s | Limited — use @sparticuz/chromium |
| Fly.io | Configurable | ~50 ms | Full Docker support |
AWS Lambda setup:
// Install: npm install @sparticuz/chromium playwright-core
const chromium = require('@sparticuz/chromium');
const { chromium: playwright } = require('playwright-core');
exports.handler = async (event) => {
const browser = await playwright.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
});
const page = await browser.newPage();
await page.setContent(event.html, { waitUntil: 'networkidle' });
const buf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();
return {
statusCode: 200,
headers: { 'Content-Type': 'application/pdf' },
body: buf.toString('base64'),
isBase64Encoded: true,
};
};
Japanese and CJK Fonts on Linux
Linux servers don't ship with Japanese fonts. Without them, Japanese characters appear as empty boxes. Fix with a Dockerfile:
FROM node:20-slim
RUN apt-get update && apt-get install -y \
chromium \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
Migrating from Playwright to FUNBREW PDF API
When your PDF generation volume grows — or when managing Chromium infrastructure becomes a drag — migrating to FUNBREW PDF API lets you keep your existing HTML templates while offloading all browser management.
Why Migrate?
| Challenge | Playwright Self-Hosted | FUNBREW PDF API |
|---|---|---|
| Memory management | Manual browser pooling | Auto-scaled by API |
| Cold start | 1–3 s per new browser | Sub-second (managed) |
| Japanese fonts | Manual Docker setup | Noto Sans JP pre-installed |
| Monitoring | Build from scratch | Built-in metrics & webhooks |
| Infrastructure cost | Server always-on | Pay per document |
| Maintenance | Playwright/Chromium version pinning | None |
Migration Code
Swap only the PDF generation function. Keep all HTML-building logic unchanged:
// Before: Playwright
const { chromium } = require('playwright');
async function generatePdf(html) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const buf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
await browser.close();
return buf;
}
// After: FUNBREW PDF API — HTML template code stays the same
async function generatePdf(html) {
const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
engine: 'quality', // Chromium-based, same rendering as Playwright
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
},
}),
});
const result = await response.json();
return result.data.download_url; // Returns a PDF URL instead of a buffer
}
The FUNBREW PDF quality engine uses Chromium — the same rendering engine as Playwright. CSS @media print rules, @page declarations, break-inside: avoid, and all other print CSS work identically.
To receive the PDF as a binary buffer instead of a URL (for streaming to the client directly), check the API documentation for the responseFormat: 'buffer' option.
Summary
page.pdf()options:format,printBackground,landscape,displayHeaderFooter,margin,outline,tagged@media printand@pagegive you full layout control in the PDF output- Production challenges: memory (150–300MB per Chromium), cold start (1–3s), serverless binary issues, and missing CJK fonts on Linux
- Migration path: replace the PDF generation function with a FUNBREW PDF API call — all HTML/CSS templates are directly reusable
Start with the Playground to test your HTML output, then explore the API docs for production configuration options.
Related Guides
- Puppeteer to PDF API Migration Guide — Side-by-side comparison of self-hosted vs managed
- CSS Tips for HTML to PDF — Page breaks, margins, and table layout
- Japanese Font Embedding in PDF — Noto Sans JP, Base64 embedding, and CJK setup
- PDF API Production Guide — Monitoring, scaling, and performance best practices
- PDF API Error Handling — Retry strategies and error responses
- Playground — Preview PDF output in real time
- API Documentation — Full FUNBREW PDF API reference