Skip to content

§ HTML to PDF Node.js

HTML to PDF Node.js: Convert HTML to PDF in JavaScript and Generate PDFs Server Side

Five real ways to turn HTML into a PDF from Node.js, and the honest tradeoff in each. Puppeteer and Playwright give you perfect Chromium fidelity if you are willing to operate a browser. Client-side libraries cannot run on your server at all. A rendering API gives you the same Chromium output as one HTTP call with nothing to install. Try it on any URL below.

  • Early access, launching soon
  • No card required
  • Your HTML stays yours

§ Live demo

Runs in your browser. Nothing is uploaded.

Your PDF is downloading. Want this as one API call, with the page archived too?

§ Short answer

To convert HTML to PDF in Node.js you need a real browser engine, and you get it one of three ways. Puppeteer or Playwright drive headless Chromium inside your own process: full CSS, web fonts and JavaScript charts, at the cost of a few hundred megabytes of Chromium in your deploy and a browser lifecycle to manage. PDFKit and pdfmake build PDFs from drawing instructions rather than HTML, so they are excellent for structured invoices and useless for rendering an existing page. html2pdf.js and jsPDF are browser-only and, in the maintainers own words, will not run in Node.js. A hosted rendering API is the fourth path: the same Chromium output as Puppeteer, reached with one HTTPS call, with no binary in your bundle. The deciding question is not fidelity, it is whether a browser belongs in your infrastructure.

Last updated 3 August 2026. Written and fact checked by the Sitepdf team.

§ 00

The five Node.js HTML to PDF routes, side by side

Every Node project lands on one of these. They differ in what they can render, what you have to install, and whether they survive a serverless deploy. Engine and support facts below come from each project own documentation.

Approach Renders real HTML and CSS What you install and operate Runs on Lambda or Vercel Best for
Puppeteer Yes, full Chromium Chromium binary, plus browser lifecycle and memory tuning Only with a slimmed Chromium layer Full control when a browser is welcome in your stack
Playwright Yes, full Chromium Browser binaries, same operational load as Puppeteer Only with a slimmed Chromium layer Teams already running Playwright for end to end tests
PDFKit or pdfmake No, you draw the document A pure JS dependency, nothing else Yes, they are small Structured invoices and reports built from data, not from a page
html2pdf.js or jsPDF Client side only Nothing on the server, because it cannot run there No, browser only A download button inside an already loaded browser tab
Hosted rendering API
Sitepdf
Yes, managed Chromium Nothing, it is an HTTPS call Yes, it is just fetch Modern CSS at volume with no browser to run, plus a dated record

The honest split: if you are generating a plain data driven invoice and never need to render an existing web page, PDFKit is a smaller and cheaper answer than anything else here and you should use it. If your document is an actual HTML template with modern CSS, you need Chromium, and the only remaining question is who runs it.

§ 01

Puppeteer is the default, and here is what it actually costs

Ask any Node developer how to make a PDF from HTML and the answer is Puppeteer. It is the right answer on fidelity. Puppeteer drives the same Chromium your users run, so flexbox, grid, Tailwind, web fonts, SVG and JavaScript rendered charts all come out exactly as they look in Chrome, and page.pdf() emits real vector text that stays selectable and searchable.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://app.example.com/invoices/8842', {
  waitUntil: 'networkidle0',
});
const pdf = await page.pdf({ format: 'Letter', printBackground: true });
await browser.close();

The cost is operational, and it is larger than it looks in a tutorial. Installing Puppeteer downloads a full Chromium build, which is a few hundred megabytes sitting in your image. Each launch() spawns a real browser process, so cold starts add roughly 300 to 800ms before a single pixel is painted, and under concurrency you are managing a pool rather than calling a function. Two failure modes bite in production: printBackground defaults to false, so backgrounds and colored table headers silently vanish unless you set it, and if anything throws between launch() and close() the Chromium process leaks, holding memory and file descriptors until the container dies. Puppeteer is a genuinely good tool. It just is not a library, it is a browser you now operate.

§ 02

Playwright: the same engine, a different reason to pick it

Playwright renders PDFs through Chromium exactly as Puppeteer does, and the output is equivalent. Note that PDF generation is Chromium only in Playwright, so the cross browser support that is its main selling point does not apply here. The real reason to choose it is that you already run it: if your test suite is Playwright, reusing that dependency and its browser management is less to maintain than adding Puppeteer alongside it.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://app.example.com/invoices/8842');
await page.pdf({ path: 'invoice.pdf', format: 'Letter', printBackground: true });
await browser.close();

Everything said about Puppeteer operations applies unchanged: the binaries are large, the browser lifecycle is yours, and serverless needs special handling. Choosing between the two is a dependency question, not a rendering one.

§ 03

Why html2pdf.js and jsPDF cannot solve this on the server

A large share of the confusion around JavaScript PDF generation comes from mixing up browser libraries with server ones. html2pdf.js is the most searched name in this space, and it cannot help a Node backend. Its own documentation is unambiguous: "html2pdf.js will not run in Node.js, it must be run in a browser."

Even in the browser, it is worth knowing what it does. html2pdf.js is a wrapper around html2canvas and jsPDF, and the README states plainly that it "renders all content into an image, then places that image into a PDF. This means text is not selectable or searchable, and causes large file sizes." That single design decision explains most of the problems people hit with it: text that cannot be copied or indexed, blurry output when zoomed, files several times larger than they should be, and, on long documents, completely blank PDFs when the page exceeds the browser maximum canvas size. Page breaks reflow because the library resizes the root element to fit a PDF page.

None of that makes it a bad library for its actual job, which is a client side download button on content already rendered in a tab. It just is not a server side HTML to PDF solution, and no amount of npm configuration will make it one. The html2pdf.js blank pages and page break guide works through the specific failures and the fix for each.

§ 04

PDFKit and pdfmake: the right answer when you are not rendering a page

There is a whole family of Node PDF libraries that never touch HTML. PDFKit gives you an imperative drawing API, and pdfmake takes a JSON document definition and lays it out. Both are pure JavaScript, both are small, both run anywhere Node runs including the tightest serverless function, and both produce real vector text.

import PDFDocument from 'pdfkit';

const doc = new PDFDocument({ size: 'LETTER' });
doc.pipe(fs.createWriteStream('invoice.pdf'));
doc.fontSize(20).text('Invoice 8842', 72, 72);
doc.fontSize(10).text('Due 15 September 2026');
doc.end();

The tradeoff is that you are writing a layout engine by hand. If your document is a fixed invoice generated from a database row, that is fine and often preferable: no browser, no CSS surprises, milliseconds per document. If your document is an existing HTML template that your designers maintain, or a page that already exists in your app, rebuilding it in drawing calls is weeks of work and every design change becomes an engineering ticket. Pick by where your document lives, not by which library is trendier.

§ 05

Why Node PDF generation breaks on Lambda and Vercel

This is where most teams abandon the local browser approach, and the reason is a hard platform quota rather than anything about your code. AWS Lambda caps the unzipped contents of a deployment package, including layers and custom runtimes, at 250 MB (the zipped upload limit is 50 MB). A standard Chromium build does not fit. That is why the workaround ecosystem exists: @sparticuz/chromium-min paired with puppeteer-core strips Chromium down far enough to squeeze under the limit, or you switch to a container image, which raises the ceiling to 10 GB but changes your whole deployment model.

// The workaround, not a solution
import chromium from '@sparticuz/chromium-min';
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.launch({
  args: chromium.args,
  executablePath: await chromium.executablePath(REMOTE_CHROMIUM_URL),
  headless: true,
});

Assume you get it deployed. The constraints keep pressing. Chromium is memory hungry, and Lambda allocates CPU in proportion to memory, with one full vCPU only arriving at 1,769 MB, so an underprovisioned function renders slowly and an adequately provisioned one is expensive to keep warm. Cold starts now include a browser launch. The 15 minute maximum timeout is generous, but a large report that renders in 40 seconds still ties up an expensive execution environment for 40 seconds. And the leaked browser problem is worse here than anywhere, because serverless containers are reused between invocations, so one unhandled throw before close() leaves a Chromium process alive to poison the next request.

Every one of those problems is caused by the browser being inside your function. Move the browser out and they all disappear at once, which is the entire argument for the API path below.

§ 06

The API path: Chromium fidelity, nothing in your bundle

A hosted rendering API runs managed Chromium on its own infrastructure. From Node it is a fetch call, so there is no binary to install, no pool to tune, no cold browser launch, and no 250 MB limit to fight. It runs identically on Lambda, Vercel, Cloud Run, a Docker container or your laptop, because from your application it is an outbound HTTPS request like any other.

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({
    url: 'https://app.example.com/invoices/8842',
    format: 'Letter',
    margin: 'normal',
    archive: true,
  }),
});

const { pdf_url, archive } = await res.json();

Send a url and Chromium loads the page the way a visitor would, or post an html string if the markup is generated in memory and never served. Output is real vector text, so it stays selectable and searchable, which is the thing the html2canvas based libraries structurally cannot give you.

The archive: true flag is the part no local package offers. It stores a timestamped snapshot of exactly what Chromium rendered, retrievable later by id, so months afterwards you can show what a given invoice or statement looked like at the moment you produced it. If you work in finance, insurance or anything with a records requirement, that dated record is usually the reason to render through an API rather than in process. See the PDF generator API page for the full call surface, website archiving for how the record works, and PDF API pricing for what each vendor in this category charges per 1,000 documents.

The honest limit: an API is a network call, so it adds latency your in process library does not have, and it will not work in an air gapped environment. If you render two documents a week, keep Puppeteer. The case for an API strengthens with volume, with serverless, and with any requirement to prove what you rendered.

§ 07

Choosing, in one pass

Work down this list and stop at the first line that matches you.

  • The PDF is built from data, not from an existing page. Use PDFKit or pdfmake. Smallest, fastest, cheapest, and it deploys anywhere.
  • The download happens in the browser, on content already on screen. Use html2pdf.js and accept that the text becomes an image, or call a rendering API from your backend if the text needs to stay selectable.
  • You render HTML server side, a browser is welcome in your infrastructure, and volume is modest. Use Puppeteer, set printBackground: true, and wrap every render in try/finally so the browser always closes.
  • You already run Playwright for tests. Use Playwright and skip the extra dependency.
  • You are on Lambda, Vercel or any serverless platform, or you are rendering at volume, or you need a dated record of what was produced. Use a rendering API. Every constraint in this section is caused by the browser living in your function.
From Node.js: render a URL in managed Chromium and keep a dated copy of exactly what was produced.
curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/invoices/8842 \
  -F format=Letter \
  -F margin=normal \
  -F archive=true

{
  "pdf_url": "https://api.sitepdf.com/v1/documents/doc_9k4tb.pdf",
  "pages": 2,
  "rendered_in_ms": 1740,
  "archive": {
    "id": "arc_x81qd",
    "captured_at": "2026-08-03T09:14:07Z",
    "retrieve_url": "https://api.sitepdf.com/v1/archives/arc_x81qd"
  }
}

The API is in early access; this is the documented call shape it opens with. Full request and response walkthrough.

§ 08

Questions about this job

How do I convert HTML to PDF in Node.js?
Use a Chromium based renderer. In process, that means Puppeteer or Playwright: launch a browser, load your URL or HTML string, and call <code>page.pdf()</code>. Out of process, it means posting the URL to a rendering API, which returns the same Chromium output without installing a browser. Pure JavaScript libraries like PDFKit build PDFs from drawing calls and do not render HTML at all.
Can html2pdf.js run in Node.js?
No. The library documentation states directly that html2pdf.js will not run in Node.js and must be run in a browser. It depends on html2canvas, which needs a live DOM and a canvas element. For server side conversion use Puppeteer, Playwright or a rendering API instead.
How do I convert HTML to PDF in Node.js without Puppeteer?
Two routes. If your document is generated from data, PDFKit or pdfmake build it directly with no browser involved. If you need real HTML and CSS rendered, call a hosted rendering API over HTTPS, which runs Chromium on its own servers so nothing is installed in your project. Playwright is not an escape from Puppeteer here, it carries the same browser binaries.
Why is my Puppeteer PDF missing background colors?
Because <code>printBackground</code> defaults to <code>false</code>. Chromium omits background colors and background images from print output unless you opt in, which is why colored table headers and hero panels disappear. Pass <code>{ printBackground: true }</code> to <code>page.pdf()</code>. If it still renders wrong, check for CSS inside a <code>@media screen</code> block that print rules never apply.
Why does Puppeteer fail on AWS Lambda?
Size, mostly. Lambda caps the unzipped deployment package including layers at 250 MB, and a standard Chromium build does not fit. The usual fixes are <code>@sparticuz/chromium-min</code> with <code>puppeteer-core</code>, or a container image, which allows 10 GB. Memory is the second wall: Lambda scales CPU with memory and one vCPU arrives only at 1,769 MB.
Is text in a generated PDF selectable?
It depends entirely on the engine. Puppeteer, Playwright, PDFKit and Chromium based APIs emit real vector text, so it stays selectable, searchable and accessible. Anything built on html2canvas, including html2pdf.js, rasterizes the page into an image first, so the result contains no text at all, only pixels. That also makes the file substantially larger.
What is the best library to generate PDF from HTML in JavaScript?
There is no single best one, because the libraries solve different problems. Puppeteer is the best in process HTML renderer. PDFKit is the best choice when the document is built from data. html2pdf.js is the best client side option when rasterized output is acceptable. For server side rendering at volume without operating a browser, a hosted API beats all three.
How long does it take to generate a PDF from HTML?
A simple page through warm Chromium typically renders in one to three seconds, dominated by page load rather than PDF encoding. Add roughly 300 to 800ms if the browser has to cold start, and more if the page waits on network requests or fonts. PDFKit style libraries finish in milliseconds because they skip layout and networking entirely.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API