These five libraries represent the two main approaches to PDF generation in JavaScript: rendering HTML/CSS to PDF or drawing documents programmatically. html-pdf and html-pdf-node convert HTML strings to PDF using headless browsers (legacy). puppeteer controls a modern headless Chrome instance for high-fidelity HTML rendering. pdfkit and pdfmake generate PDFs from scratch using JavaScript commands or a specific document definition structure, offering precise control without HTML dependencies.
Generating PDFs in a JavaScript environment usually falls into two categories: rendering HTML/CSS or drawing elements programmatically. The five packages listed here cover both approaches, but they differ significantly in maintenance status, resource usage, and developer experience. Let's compare how they handle common engineering tasks.
html-pdf and html-pdf-node rely on PhantomJS, a headless browser that is no longer maintained. They take an HTML string and render it. This is easy for web developers but risky for long-term projects.
// html-pdf
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {});
// html-pdf-node
const pdf = require('html-pdf-node');
pdf.create('<h1>Hello</h1>').toFile('out.pdf');
puppeteer controls a real, modern headless Chrome instance. It renders HTML/CSS exactly as a browser would, supporting modern standards like Flexbox and Grid.
// puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent('<h1>Hello</h1>');
await page.pdf({ path: 'out.pdf' });
await browser.close();
})();
pdfkit and pdfmake do not use HTML. They draw directly to the PDF specification. pdfkit is low-level, while pdfmake uses a document definition object.
// pdfkit
const PDFDocument = require('pdfkit');
const doc = new PDFDocument();
doc.pipe(require('fs').createWriteStream('out.pdf'));
doc.text('Hello', 100, 100);
doc.end();
// pdfmake
const pdfmake = require('pdfmake/build/pdfmake');
const docDefinition = { content: ['Hello'] };
pdfmake.createPdf(docDefinition).download('out.pdf');
Dependency weight matters for serverless functions or containerized environments. Heavy browsers increase cold start times and image sizes.
html-pdf and html-pdf-node require PhantomJS binaries. This often leads to installation errors on modern Linux distributions or ARM architectures.
// html-pdf
// Requires phantomjs-prebuilt (deprecated)
npm install html-pdf
// html-pdf-node
// Similar binary dependencies
npm install html-pdf-node
puppeteer downloads a full Chromium browser by default. This is large (hundreds of MBs) but reliable. You can configure it to use a system browser to save space.
// puppeteer
// Downloads Chromium automatically
npm install puppeteer
// Or use slim version without browser
npm install puppeteer-core
pdfkit and pdfmake are pure JavaScript with no external browser binaries. They install quickly and run in any environment that supports Node.js.
// pdfkit
// Pure JS, no binaries
npm install pdfkit
// pdfmake
// Pure JS, no binaries
npm install pdfmake
How you style the output determines how easy it is for your team to maintain the code. Web developers prefer CSS; data-heavy apps often prefer JSON structures.
html-pdf and html-pdf-node use standard CSS. This is familiar but limited by PhantomJS's outdated CSS support (no Grid, limited Flexbox).
// html-pdf
const html = `<div style="display: flex;"><h1>Title</h1></div>`;
// Note: Flexbox support is poor in PhantomJS
// html-pdf-node
const html = `<div style="display: flex;"><h1>Title</h1></div>`;
// Same limitations as html-pdf
puppeteer supports full modern CSS. You can use external stylesheets, media queries for print, and complex layouts.
// puppeteer
await page.setContent(`
<style>@media print { .no-print { display: none; } }</style>
<div style="display: grid;"><h1>Title</h1></div>
`);
pdfkit requires manual positioning or using helper plugins for layout. You specify X and Y coordinates for text.
// pdfkit
doc.fontSize(25).text('Title', 100, 100);
doc.moveTo(100, 110).lineTo(500, 110).stroke();
pdfmake uses a content array with style objects. It handles line breaks and tables automatically without coordinates.
// pdfmake
const dd = {
content: [
{ text: 'Title', style: 'header' },
{ text: 'Body text', style: 'body' }
],
styles: { header: { fontSize: 25 } }
};
Choosing a library is a commitment to future maintenance. Deprecated tools introduce security risks and build failures.
html-pdf is officially deprecated. PhantomJS development stopped in 2018. Do not use this for new projects.
// html-pdf
// ⛔ DEPRECATED: Do not use in new projects
// Repository archived, no security updates
html-pdf-node is a community fork. It may work today but lacks the backing of a major organization. It is a stop-gap solution.
// html-pdf-node
// ⚠️ RISK: Community fork, long-term support uncertain
// Use only for legacy migration
puppeteer is maintained by the Chrome DevTools team. It updates regularly to match the latest Chrome features.
// puppeteer
// ✅ ACTIVE: Maintained by Google Chrome team
// Regular security and feature updates
pdfkit and pdfmake are stable, mature libraries. They change slowly because the PDF specification does not change often.
// pdfkit & pdfmake
// ✅ STABLE: Mature libraries with long-term support
// Ideal for stable document generation pipelines
Despite their differences, all these tools can generate valid PDF files for download or storage.
All packages can save directly to the disk or return a buffer for streaming.
// html-pdf
pdf.create(html).toFile('path/to/file.pdf');
// html-pdf-node
pdf.create(html).toFile('path/to/file.pdf');
// puppeteer
await page.pdf({ path: 'path/to/file.pdf' });
// pdfkit
doc.pipe(fs.createWriteStream('path/to/file.pdf'));
// pdfmake
pdfMake.createPdf(dd).download('file.pdf');
All five run in Node.js environments, making them suitable for backend APIs.
// All packages
// Express.js example pattern
app.get('/invoice', async (req, res) => {
// Generate PDF buffer
// res.send(buffer);
});
All tools run on the server, keeping template logic away from the client. However, browser-based tools (puppeteer, html-pdf) must sanitize HTML to prevent XSS if rendering user input.
// puppeteer (Sanitization required)
await page.setContent(sanitize(userInput));
// pdfkit (Safe by default)
doc.text(safeString, x, y);
| Feature | html-pdf | html-pdf-node | pdfkit | pdfmake | puppeteer |
|---|---|---|---|---|---|
| Engine | PhantomJS | PhantomJS | Pure JS | Pure JS | Chromium |
| Status | ⛔ Deprecated | ⚠️ Legacy Fork | ✅ Stable | ✅ Stable | ✅ Active |
| Input | HTML/CSS | HTML/CSS | Commands | JSON Def | HTML/CSS |
| CSS Support | Poor | Poor | N/A | N/A | Excellent |
| Size | Medium | Medium | Small | Small | Large |
html-pdf and html-pdf-node are legacy tools. They should only exist in your codebase during a migration away from them. Relying on PhantomJS in 2024 introduces unnecessary risk.
pdfkit is the choice for engineers who need to draw custom graphics or have precise control over every millimeter of the page. It has a steeper learning curve but offers maximum flexibility.
pdfmake strikes a balance. It is easier than pdfkit for standard documents but does not support HTML. It is perfect for internal reports where you control the data structure.
puppeteer is the modern standard for HTML-to-PDF. If your team knows CSS and HTML, this is the fastest path to high-quality output. The resource cost is higher, but the fidelity is unmatched.
Final Thought: For new projects, avoid the PhantomJS-based tools entirely. Choose puppeteer for web-like documents and pdfmake or pdfkit for data-driven reports.
Use puppeteer when you need pixel-perfect reproduction of existing HTML and CSS, such as converting web pages or complex email templates to PDF. It is the modern standard for HTML-to-PDF conversion, though it requires more resources due to the Chromium dependency.
Select pdfkit when you need low-level control over every element on the page, such as drawing custom vectors, lines, or positioning text at exact coordinates. It is ideal for generating invoices, tickets, or reports where layout precision matters more than HTML styling.
Avoid this package for new projects as it is deprecated and relies on PhantomJS, which is no longer maintained. Only use it if you are maintaining a legacy system that cannot be refactored immediately. For any new work, migrate to puppeteer for HTML-based rendering.
Choose this only as a temporary bridge for legacy code that depends on the html-pdf API but needs to run on newer Node versions. It is not a long-term solution. Plan to refactor to puppeteer or a programmatic library like pdfkit for better stability.
Pick pdfmake if you want to generate structured documents like reports or forms without writing HTML or managing low-level drawing commands. It uses a simple JSON-like definition to handle layout, making it easier to maintain than pdfkit for standard business documents.
Puppeteer is a JavaScript library which provides a high-level API to control Chrome or Firefox over the DevTools Protocol or WebDriver BiDi. Puppeteer runs in the headless (no visible UI) by default
npm i puppeteer # Downloads compatible Chrome during installation.
npm i puppeteer-core # Alternatively, install as a library, without downloading Chrome.
Install chrome-devtools-mcp,
a Puppeteer-based MCP server for browser automation and debugging.
import puppeteer from 'puppeteer';
// Or import puppeteer from 'puppeteer-core';
// Launch the browser and open a new blank page.
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Navigate the page to a URL.
await page.goto('https://developer.chrome.com/');
// Set screen size.
await page.setViewport({width: 1080, height: 1024});
// Open the search menu using the keyboard.
await page.keyboard.press('/');
// Type into search box using accessible input name.
await page.locator('::-p-aria(Search)').fill('automate beyond recorder');
// Wait and click on first result.
await page.locator('.devsite-result-item-link').click();
// Locate the full title with a unique string.
const textSelector = await page
.locator('::-p-text(Customize and automate)')
.waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);
// Print the full title.
console.log('The title of this blog post is "%s".', fullTitle);
await browser.close();