Best npm Packages for PDF Generation 2026
·PkgPulse Team
TL;DR
For programmatic PDFs (invoices, reports): PDFKit or @react-pdf/renderer. For HTML-to-PDF (screenshots of web pages): Puppeteer or Playwright. For dynamic PDFs from templates: pdfmake or jsPDF. Avoid Puppeteer for high-volume PDF generation — headless Chrome is expensive. Use PDFKit for server-side performance.
Key Takeaways
- PDFKit: Low-level PDF generation, Node.js native, ~500KB, no browser needed
- @react-pdf/renderer: React components → PDF, great for invoice/report templates
- Puppeteer/Playwright: HTML → PDF (screenshot-based), best for pixel-perfect web pages
- pdfmake: JSON document definition → PDF, good for tabular reports
- 2026 winner by use case: PDFKit (server perf) | @react-pdf (dev experience) | Playwright (web-to-PDF)
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
pdfkit | ~1.2M | → Stable |
@react-pdf/renderer | ~800K | ↑ Growing |
jspdf | ~2.5M | → Stable |
pdfmake | ~500K | → Stable |
PDFKit: Low-Level Server PDF
npm install pdfkit
// Programmatic PDF generation with PDFKit:
import PDFDocument from 'pdfkit';
import { createWriteStream } from 'fs';
// Invoice PDF:
export async function generateInvoice(invoice: Invoice): Promise<Buffer> {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({ margin: 50 });
const chunks: Buffer[] = [];
doc.on('data', (chunk) => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', reject);
// Header:
doc
.fontSize(24)
.font('Helvetica-Bold')
.text('INVOICE', 50, 50)
.fontSize(10)
.font('Helvetica')
.text(`Invoice #${invoice.number}`, { align: 'right' })
.text(new Date(invoice.date).toLocaleDateString(), { align: 'right' });
// Company info:
doc
.moveDown(2)
.fontSize(12)
.text(invoice.companyName, 50)
.fontSize(10)
.text(invoice.companyAddress)
.text(invoice.companyEmail);
// Bill to:
doc
.moveDown()
.fontSize(12)
.text('Bill To:', 50)
.fontSize(10)
.text(invoice.clientName)
.text(invoice.clientEmail);
// Line items table:
doc.moveDown(2);
const tableTop = doc.y;
// Table headers:
doc
.fontSize(10)
.font('Helvetica-Bold')
.text('Description', 50, tableTop)
.text('Qty', 300, tableTop)
.text('Price', 370, tableTop)
.text('Total', 440, tableTop);
// Horizontal line:
doc
.moveTo(50, tableTop + 15)
.lineTo(550, tableTop + 15)
.stroke();
// Line items:
doc.font('Helvetica');
let y = tableTop + 25;
for (const item of invoice.items) {
doc
.text(item.description, 50, y)
.text(item.quantity.toString(), 300, y)
.text(`$${item.price.toFixed(2)}`, 370, y)
.text(`$${(item.quantity * item.price).toFixed(2)}`, 440, y);
y += 20;
}
// Total:
doc
.moveTo(50, y + 10)
.lineTo(550, y + 10)
.stroke()
.font('Helvetica-Bold')
.text('Total:', 370, y + 20)
.text(`$${invoice.total.toFixed(2)}`, 440, y + 20);
doc.end();
});
}
// Serve PDF in Next.js API route:
// app/api/invoice/[id]/route.ts
export async function GET(req: Request, { params }: { params: { id: string } }) {
const invoice = await db.invoice.findUnique({
where: { id: params.id },
include: { items: true, client: true },
});
if (!invoice) return new Response('Not found', { status: 404 });
const pdf = await generateInvoice(invoice);
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`,
'Content-Length': pdf.length.toString(),
},
});
}
@react-pdf/renderer: React Components → PDF
npm install @react-pdf/renderer
// React-based PDF document — great for invoices, reports:
import {
Document, Page, Text, View, StyleSheet, PDFDownloadLink,
Font, Image,
} from '@react-pdf/renderer';
// Register custom font:
Font.register({
family: 'Inter',
fonts: [
{ src: '/fonts/Inter-Regular.ttf' },
{ src: '/fonts/Inter-Bold.ttf', fontWeight: 'bold' },
],
});
const styles = StyleSheet.create({
page: { padding: 40, fontFamily: 'Inter', fontSize: 10 },
header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 30 },
title: { fontSize: 24, fontWeight: 'bold', color: '#1a1a1a' },
table: { display: 'flex', width: 'auto', marginTop: 20 },
tableRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingVertical: 8 },
tableHeader: { backgroundColor: '#f9fafb', fontWeight: 'bold' },
col1: { width: '50%' },
col2: { width: '15%', textAlign: 'center' },
col3: { width: '17.5%', textAlign: 'right' },
col4: { width: '17.5%', textAlign: 'right' },
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 16 },
totalLabel: { fontWeight: 'bold', marginRight: 8 },
});
function InvoicePDF({ invoice }: { invoice: Invoice }) {
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.title}>INVOICE</Text>
<Text style={{ color: '#6b7280', marginTop: 4 }}>#{invoice.number}</Text>
</View>
<View style={{ textAlign: 'right' }}>
<Text style={{ fontWeight: 'bold' }}>{invoice.companyName}</Text>
<Text style={{ color: '#6b7280' }}>{invoice.companyEmail}</Text>
<Text style={{ color: '#6b7280' }}>{new Date(invoice.date).toLocaleDateString()}</Text>
</View>
</View>
{/* Bill To */}
<View style={{ marginBottom: 20 }}>
<Text style={{ fontWeight: 'bold', marginBottom: 4 }}>Bill To:</Text>
<Text>{invoice.clientName}</Text>
<Text style={{ color: '#6b7280' }}>{invoice.clientEmail}</Text>
</View>
{/* Line Items Table */}
<View style={styles.table}>
{/* Table Header */}
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={styles.col1}>Description</Text>
<Text style={styles.col2}>Qty</Text>
<Text style={styles.col3}>Price</Text>
<Text style={styles.col4}>Total</Text>
</View>
{/* Items */}
{invoice.items.map((item, i) => (
<View key={i} style={styles.tableRow}>
<Text style={styles.col1}>{item.description}</Text>
<Text style={styles.col2}>{item.quantity}</Text>
<Text style={styles.col3}>${item.price.toFixed(2)}</Text>
<Text style={styles.col4}>${(item.quantity * item.price).toFixed(2)}</Text>
</View>
))}
</View>
{/* Total */}
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Total:</Text>
<Text style={{ fontWeight: 'bold', fontSize: 14 }}>${invoice.total.toFixed(2)}</Text>
</View>
</Page>
</Document>
);
}
// Download button in React:
export function DownloadInvoiceButton({ invoice }: { invoice: Invoice }) {
return (
<PDFDownloadLink
document={<InvoicePDF invoice={invoice} />}
fileName={`invoice-${invoice.number}.pdf`}
>
{({ loading }) => (loading ? 'Generating...' : 'Download PDF')}
</PDFDownloadLink>
);
}
// Server-side generation with @react-pdf/renderer:
import { renderToBuffer } from '@react-pdf/renderer';
export async function generateInvoicePDF(invoice: Invoice): Promise<Buffer> {
return renderToBuffer(<InvoicePDF invoice={invoice} />);
}
Playwright/Puppeteer: HTML → PDF
// Playwright HTML-to-PDF (best for web page screenshots):
import { chromium } from 'playwright';
export async function htmlToPDF(html: string): Promise<Buffer> {
const browser = await chromium.launch();
const page = await browser.newPage();
// Set HTML content directly (no server needed):
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true, // Include background colors/images
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
});
await browser.close();
return pdf;
}
// Or navigate to a URL:
export async function urlToPDF(url: string): Promise<Buffer> {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
// Wait for any lazy content:
await page.waitForTimeout(500);
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:10px;text-align:right;width:100%;padding-right:10mm"><span class="date"></span></div>',
footerTemplate: '<div style="font-size:10px;text-align:center;width:100%"><span class="pageNumber"></span> / <span class="totalPages"></span></div>',
});
await browser.close();
return pdf;
}
// Reuse browser for performance (don't launch per request):
import { Browser, chromium } from 'playwright';
let browser: Browser | null = null;
async function getBrowser() {
if (!browser || !browser.isConnected()) {
browser = await chromium.launch();
}
return browser;
}
export async function htmlToPDFOptimized(html: string): Promise<Buffer> {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await context.close(); // Close context, not browser
return pdf;
}
pdfmake: JSON Document Definition
npm install pdfmake
npm install -D @types/pdfmake
// pdfmake — define document as JSON:
import PdfPrinter from 'pdfmake';
import { TDocumentDefinitions } from 'pdfmake/interfaces';
// Built-in fonts:
const fonts = {
Roboto: {
normal: 'node_modules/pdfmake/build/vfs_fonts.js',
bold: 'node_modules/pdfmake/build/vfs_fonts.js',
},
};
const printer = new PdfPrinter(fonts);
export function generateReport(data: ReportData): Buffer {
const docDefinition: TDocumentDefinitions = {
content: [
{ text: 'Monthly Report', style: 'header' },
{ text: `Period: ${data.period}`, style: 'subheader' },
// Summary table:
{
table: {
headerRows: 1,
widths: ['*', 'auto', 'auto', 'auto'],
body: [
// Header row:
[
{ text: 'Metric', style: 'tableHeader' },
{ text: 'Current', style: 'tableHeader' },
{ text: 'Previous', style: 'tableHeader' },
{ text: 'Change', style: 'tableHeader' },
],
// Data rows:
...data.metrics.map(m => [
m.name,
m.current.toString(),
m.previous.toString(),
{ text: `${m.changePercent}%`, color: m.changePercent >= 0 ? 'green' : 'red' },
]),
],
},
layout: 'lightHorizontalLines',
margin: [0, 10, 0, 20],
},
// Chart placeholder:
{ text: 'Key Insights', style: 'subheader' },
{
ul: data.insights.map(insight => ({ text: insight })),
},
],
styles: {
header: { fontSize: 20, bold: true, margin: [0, 0, 0, 10] },
subheader: { fontSize: 14, bold: true, margin: [0, 15, 0, 5] },
tableHeader: { bold: true, fillColor: '#f3f4f6' },
},
defaultStyle: { font: 'Roboto', fontSize: 10 },
};
const doc = printer.createPdfKitDocument(docDefinition);
const chunks: Buffer[] = [];
return new Promise((resolve) => {
doc.on('data', chunk => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.end();
}) as unknown as Buffer;
}
Performance Benchmark
PDF Generation (100 invoices):
PDFKit (pure Node.js): ~0.8s total (~8ms/PDF)
@react-pdf/renderer (server): ~3.2s total (~32ms/PDF)
pdfmake: ~1.4s total (~14ms/PDF)
Playwright (browser-based): ~18s total (~180ms/PDF) *
jsPDF (client-side): Client only — N/A server
* Browser startup: ~800ms (amortized with persistent browser instance)
* Per-page with persistent browser: ~25ms/PDF
Memory usage:
PDFKit: ~20MB RSS
@react-pdf: ~45MB RSS (JSX runtime overhead)
Playwright: ~150MB RSS (Chromium process)
Decision Guide
Use PDFKit if:
→ Maximum server performance (8ms/PDF)
→ Programmatic layout (no templates)
→ Memory-constrained environments
→ Simple documents (invoices, receipts, reports)
Use @react-pdf/renderer if:
→ React component model (JSX templates)
→ Design-heavy PDFs (custom fonts, layouts)
→ Team already knows React — DX wins
→ Client-side PDF download in browser
Use Playwright/Puppeteer HTML→PDF if:
→ Need pixel-perfect render of existing web pages
→ Print-to-PDF of dashboards or reports
→ Can't build PDF template from scratch
→ CSS styling is a must (gradients, complex layouts)
Use pdfmake if:
→ JSON-driven templates (data-heavy reports)
→ Dynamic tables and complex grids
→ Need VFS fonts (self-contained)
Use jsPDF if:
→ Client-side only (browser PDF generation)
→ Simple documents, no server involvement
→ Need canvas-based image exports
Avoid for high-volume PDF:
→ Puppeteer launched per request (too slow)
→ @react-pdf server rendering for >1000/min
(use PDFKit or a PDF queue instead)
Compare PDF generation packages on PkgPulse.