The journal · 19 July 2026 · 9 min read
HTML to PDF in Node.js: the options that actually work in 2026
Node has no shortage of HTML to PDF packages, and most of them are a thin wrapper around headless Chrome or an abandoned binary. Here is the current map: which ones render modern CSS, what running the browser actually costs, and the working code for each.
Node has a dozen packages that claim to turn HTML into a PDF, and almost all of them fall into three buckets: a wrapper around headless Chrome (Puppeteer, Playwright, html-pdf-node), a wrapper around the abandoned wkhtmltopdf binary, or a pure JavaScript drawing library that does not really render HTML at all. The one you want depends entirely on how faithful the output must be and whether you are willing to run a browser in production. Here is the current map, with working code and the tradeoffs that actually decide it.
The short version
For a PDF that matches a modern browser, you need a real browser engine, and in Node that means Puppeteer or Playwright driving headless Chrome. Both produce excellent output because Chromium does the rendering, and both hand you the same operational bill: a browser to install, patch and babysit in production. If you do not want to run that browser, call a rendering API that runs it for you. Avoid the packages that shell out to wkhtmltopdf; that binary was archived in 2023 with an unpatched critical CVE. And know that libraries like jsPDF do not render your HTML at all, they draw shapes, so they only fit hand built documents.
Puppeteer: the default, and what it costs
Puppeteer is the standard answer. It launches headless Chrome, loads your HTML or a URL, and calls Chrome's own page.pdf(). The output is exactly what Chrome would print, because it is Chrome printing.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'Letter',
printBackground: true, // Chrome drops backgrounds by default
margin: { top: '1in', bottom: '1in', left: '1in', right: '1in' },
});
await browser.close();
Two lines earn their keep. waitUntil: 'networkidle0' lets late loading content and fonts settle before the capture, and printBackground: true stops Chromium stripping background colors and images in print mode, which is the single most common reason a rendered PDF looks washed out next to the screen. The output quality is not the problem with Puppeteer. The problem is everything around it: you are now running a headless browser, which means installing Chrome on every server and container, keeping it patched, and absorbing the reality that headless Chrome leaks memory, spikes CPU and falls over under concurrency. Under load you end up building a pool of browser instances and a queue in front of it. The library versus API comparison puts real numbers on that cost.
Playwright: the same idea, arguably better ergonomics
Playwright does the same job with a nicer API and manages its own browser binaries, which many teams find less fragile than Puppeteer's setup. For PDF generation specifically the two are close, and either is a fine choice; the differences that matter show up more in testing and multi browser automation than in page.pdf(). If you are picking between them for PDF work, the full breakdown is in the Playwright versus Puppeteer for PDF guide.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({ format: 'Letter', printBackground: true });
await browser.close();
The operational cost is identical to Puppeteer, because it is the same headless Chrome underneath. Choosing Playwright does not get you out of running a browser; it just makes running it a little smoother.
html-pdf-node and friends: a thin wrapper, same engine
Packages like html-pdf-node exist to save you the Puppeteer boilerplate: you pass HTML and options, it drives Chrome for you and returns a buffer. That is convenient, and it is still Puppeteer underneath, so it carries the exact same requirement that headless Chrome be installed and maintained wherever your code runs. A wrapper hides the setup; it does not remove the browser. Judge these packages on whether they are actively maintained and let you reach the underlying Puppeteer options you need, not on the promise that they are somehow lighter. They are not.
The ones to avoid, and the ones that are not really renderers
Older packages such as html-pdf shell out to wkhtmltopdf. That binary was archived in January 2023, ships a frozen pre-modern WebKit, and carries CVE-2022-35583, a server side request forgery flaw rated 9.8 that will not be patched. Do not start a new Node project on it; the full case is in the wkhtmltopdf alternative page.
Separately, jsPDF and PDFKit are not HTML renderers. They are drawing libraries: you place text, lines and boxes at coordinates. jsPDF has an html() helper, but it leans on html2canvas and effectively rasterizes a screenshot, which gives you a fuzzy, unsearchable image of your page rather than real text. These are the right tool only when you are constructing a document programmatically from data and never had HTML to begin with. If you have a styled HTML template, they are the wrong category. The browser side of this, and where jsPDF genuinely fits, is covered in the HTML to PDF in JavaScript guide, and if your markup comes out of a React app, the React to PDF page explains why the same screenshot approach disappoints there too.
A rendering API: the same Chrome, without owning it
The other way to get real browser output from Node is to let a service run the browser. Your code makes one HTTP request, and there is no Chrome to install, no memory to manage, and nothing that changes when you move to a smaller container or a serverless function.
const res = await fetch('https://api.sitepdf.com/v1/render', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SITEPDF_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html, // or url: 'https://example.com/report'
format: 'Letter',
printBackground: true,
archive: true, // keep a timestamped copy of this document
}),
});
const { pdf_url, archive } = await res.json();
This matters most in two places. On serverless, bundling Chrome into a Lambda function fights the 50 MB limit and cold starts, a whole saga covered in running Puppeteer PDF on AWS Lambda; an API sidesteps all of it. And when you need a record, archive=true stores a timestamped snapshot of exactly what was rendered, retrievable later, which no local library gives you. The tradeoff is honest: you depend on a service and pay per render instead of paying in servers and ops time. The Puppeteer alternative page lays out when that trade is worth it.
Which one to reach for
| Your situation | Use this |
|---|---|
| Modern CSS, you run your own servers and want control | Puppeteer or Playwright directly |
| Same, but you want less Puppeteer boilerplate | A maintained wrapper like html-pdf-node (still Chrome) |
| Serverless, small containers, or no appetite to run a browser | A rendering API |
| You need a dated record of every document | An API with archiving |
| Building a document from data with no HTML | PDFKit or jsPDF (drawing, not rendering) |
| Any package that wraps wkhtmltopdf | Stop; pick one of the above instead |
The real fork is whether you want to own the browser. If yes, Puppeteer or Playwright are excellent and free, and the work is operational. If no, an API gives you the same Chromium output for an HTTP request and a per render fee. Everything else is detail. One thing that often precedes the render is assembling the HTML from data that lives in several systems at once, a billing table here, customer details there, line items from a third place; when that data is scattered, a layer that connects your apps, APIs and databases can pull it together before you build the page you are about to convert.
For the neighboring language guides, see HTML to PDF in Python, HTML to PDF in PHP and HTML to PDF in Go, where chromedp plays the same role Puppeteer does here. If the conclusion you are reaching is that you would rather not run a browser at all, HTML to PDF without headless Chrome lays out the two honest ways to avoid one. For the full field of hosted options with pricing, see the seven tool comparison.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.