Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For modern HTML, CSS, web fonts, charts and JavaScript, start with a maintained browser engine: Puppeteer or Playwright. They render the page as a browser does, then expose print controls for page size, margins, headers, footers, scaling and fonts. Choose PDFKit when your document is a fixed layout that can be drawn directly in code. Use html-pdf-node when you want a smaller wrapper around Puppeteer without changing its Chromium runtime.

Choose by rendering model

The right npm package depends on whether your source is already a web page or a document specification. Browser engines execute HTML, CSS and client-side JavaScript. Programmatic generators build a PDF through their own drawing and text APIs.

Package or approach HTML/CSS/JavaScript fidelity Authoring model Operational cost Best fit
Puppeteer High for pages Chromium can render; JavaScript and web fonts run before capture Navigate to a URL or set page content, then call page.pdf() Requires a compatible browser binary and fonts Existing React, Vue, SSR pages, dashboards and charts
Playwright High with its browser engines; use the Chromium project for PDF generation Browser automation API with explicit waits, contexts and PDF options Browser binaries and container sandboxing must be managed Teams already using Playwright or needing its automation controls
html-pdf-node Same underlying Chromium behavior as Puppeteer Small wrapper around an HTML input and an options object Still includes Puppeteer’s browser dependency Simple conversions where wrapper convenience matters
PDFKit Does not interpret arbitrary browser CSS or JavaScript Draw text, shapes and images through a streaming document API No browser startup; your code owns layout Invoices, certificates and fixed-layout reports
PhantomJS or wkhtmltopdf wrappers Legacy engines can lag current CSS and JavaScript Compatibility-oriented HTML conversion Older binaries and differing rendering behavior Only when existing fixtures require the legacy output

When Puppeteer is the natural default

Puppeteer’s PDF API generates using the print CSS media type by default. That makes it a close match for a web page that already has print styles. If the PDF should look like the on-screen page, call emulateMediaType('screen') before generating it. The API also exposes page format, margins, scale, headers and footers, background printing, CSS page-size preference and a way to wait for fonts.

Use Puppeteer when the page includes client-side charts, web components, authenticated data or layout rules that would be expensive to reproduce in a drawing library. The trade-off is a browser worker: production must provide a compatible Chromium binary, required fonts, network access to assets and a safe sandbox configuration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When Playwright is the better browser choice

Playwright is a strong choice when the application already uses Playwright for end-to-end tests or when its browser contexts, request controls and locator-based waits simplify your capture flow. Create a context with the required locale, color scheme or device settings, navigate, wait for the application’s ready signal and call page.pdf(). As with Puppeteer, decide explicitly between print and screen media and wait for fonts and late charts before capture.

Do not select Playwright expecting PDFKit-style lightweight generation. A browser engine is still required, so image size, cold starts, sandbox permissions and concurrency need the same operational planning as Puppeteer.

Where html-pdf-node fits

html-pdf-node is a convenience wrapper around Puppeteer. Its options include format, margins, scale and preferCSSPageSize, which can reduce glue code for a straightforward conversion. It does not replace or shrink the Chromium runtime. If you need custom waits, authentication, request interception, clicks or detailed error handling, use Puppeteer directly so those controls are visible in your code.

When PDFKit is the right abstraction

PDFKit is a PDF document generation library for Node and the browser. You place text and graphics at coordinates, choose fonts, and stream the result to a file or response. That is often simpler for a known invoice, certificate or form than launching a browser.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The boundary is important: PDFKit is not a drop-in renderer for a complex website. It will not execute React, apply your existing CSS cascade or run chart JavaScript. Rebuilding a page in PDFKit means owning line wrapping, pagination, tables, images and font metrics yourself. Choose it when that direct control is the goal, not when the source of truth is an arbitrary HTML page.

Pagination and print controls that decide quality

Media type

Print media can hide navigation, change colors or use different spacing. Keep those rules if you want a print document; switch to screen media only when the visual design intentionally follows the browser view.

Page size and margins

Set a format such as A4 or Letter, or let an @page rule define the size with preferCSSPageSize. Define margins in one place and verify that header and footer space is included. A mismatch between CSS and API margins is a common source of clipped content.

Headers, footers and page numbers

Browser PDF APIs accept small HTML templates for headers and footers. Keep them self-contained: external page styles and application JavaScript are not automatically available inside the template. Test page numbers, long titles and the first page separately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fonts, images and charts

Wait for document.fonts.ready before capture. Ensure the worker can reach image, stylesheet and font URLs, and use a deterministic ready selector after charts finish drawing. Otherwise the PDF may contain fallback fonts, empty chart canvases or unstyled markup.

Page breaks

Use break-before, break-after, break-inside and table-specific rules in print CSS. Long rows, non-breaking flex items and oversized images can still force unexpected breaks, so include representative long content in visual tests.

Runnable Node.js examples

Install only the package you select

npm install puppeteer
npm install playwright
npm install html-pdf-node
npm install pdfkit

These commands are alternatives; a service normally installs only the library it uses. Browser packages also download or expect a browser binary according to their installation configuration.

Puppeteer: URL to a paginated PDF

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/report', {
    waitUntil: 'networkidle2',
    timeout: 90000
  });
  await page.emulateMediaType('screen');
  await page.evaluate(async () => {
    if (document.fonts) await document.fonts.ready;
  });
  await page.waitForSelector('[data-report-ready]', { timeout: 30000 });
  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true,
    displayHeaderFooter: true,
    headerTemplate: '<span></span>',
    footerTemplate: '<span style="font-size:9px"><span class="pageNumber"></span> / <span class="totalPages"></span></span>',
    margin: { top: '18mm', right: '14mm', bottom: '18mm', left: '14mm' }
  });
} finally {
  await browser.close();
}

Replace the ready selector with one emitted by your application. For a static page, remove that wait and keep the font wait.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Playwright: controlled browser context

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
try {
  const context = await browser.newContext({
    colorScheme: 'light',
    locale: 'en-US'
  });
  const page = await context.newPage();
  await page.goto('https://example.com/report', {
    waitUntil: 'networkidle',
    timeout: 90000
  });
  await page.emulateMedia({ media: 'screen' });
  await page.evaluate(async () => {
    if (document.fonts) await document.fonts.ready;
  });
  await page.locator('[data-report-ready]').waitFor({ timeout: 30000 });
  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true,
    margin: { top: '18mm', right: '14mm', bottom: '18mm', left: '14mm' }
  });
  await context.close();
} finally {
  await browser.close();
}

html-pdf-node: a small wrapper

import fs from 'node:fs/promises';
import pdf from 'html-pdf-node';

const file = {
  content: '<!doctype html><html><head><style>@page{size:A4;margin:18mm}body{font-family:Arial}</style></head><body><h1>Invoice</h1><p>Paid</p></body></html>'
};
const options = {
  format: 'A4',
  printBackground: true,
  preferCSSPageSize: true,
  margin: { top: '0', right: '0', bottom: '0', left: '0' }
};
const buffer = await pdf.generatePdf(file, options);
await fs.writeFile('invoice.pdf', buffer);

Use the wrapper for uncomplicated input. For a URL that needs login, cookies or a custom readiness condition, direct Puppeteer is usually clearer.

PDFKit: fixed-layout streaming

import fs from 'node:fs';
import PDFDocument from 'pdfkit';

const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('certificate.pdf'));
doc.fontSize(24).text('Certificate', { align: 'center' });
doc.moveDown();
doc.fontSize(13).text('Awarded to Ada Lovelace', { align: 'center' });
doc.moveDown(2);
doc.fontSize(10).text('Issued 30 September 2026', { align: 'center' });
doc.end();

For an HTTP response, pipe the document to the response instead of a file and set the PDF content type. More elaborate documents should centralize measurements, reusable components and page-break logic so the layout remains testable.

Deployment, performance and reliability

Browser workers

Launch one browser process and create short-lived pages or contexts rather than launching Chromium for every request. Limit concurrent pages according to memory, close every page in a finally block and recycle workers that accumulate leaks. In containers, install the exact browser revision expected by the package and provide the fonts used by your CSS.

Waiting strategy

networkidle is useful but not sufficient for applications that poll, stream or keep analytics connections open. A deterministic application marker such as data-report-ready is safer. Add a bounded timeout so a broken page cannot hold a worker indefinitely.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Assets and authentication

Pass cookies, authorization headers or a logged-in context before navigation. Verify that private assets are reachable from the production network. If a page depends on third-party scripts, decide whether to allow them or replace them with deterministic fixtures for reproducible output.

Cost and capacity

There is no reliable universal speed benchmark for these packages: output time varies with page complexity, browser startup, fonts, network and concurrency. Measure your own representative pages, including the largest tables and charts. PDFKit avoids browser startup, while browser approaches trade that overhead for web-platform fidelity.

Visual regression tests

Keep fixture pages for print and screen media, long text, missing images, web fonts, charts and multi-page tables. Compare rendered PDFs or page images in CI after upgrading the package or browser, because engine updates can change line wrapping and pagination.

Legacy wrappers: migrate deliberately

PhantomJS-based node-html-pdf and wkhtmltopdf wrappers can be adequate for an existing, frozen template, but their older engines may not understand current CSS or JavaScript. Treat continued use as a compatibility decision. Before migrating, capture representative fixtures with the old and new engines and review every intentional visual difference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need a managed endpoint rather than operating browser workers, ScreenshotNeo is the alternative to try first. It can return a PDF from one request and handles the browser setup for you. Cookie and consent banners, newsletter popups and chat widgets are removed before capture; bot checks, blank pages and failed loads are never billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.

See the ScreenshotNeo API documentation for the current parameters. A cURL call is:

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

import requests
r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
    timeout=90
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

The Free plan includes 1,000 shots each month with no card. Paid plans are Starter ($5 for 3,000), Growth ($15 for 15,000), Pro ($39 for 60,000), Scale ($99 for 250,000) and Business ($249 for 1,000,000); yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to try the endpoint without a card.

Troubleshooting

Symptom Likely cause Fix
PDF looks different from the website Print media rules are active Keep print CSS if that is intended; otherwise emulate screen media before pdf().
Fonts fall back or text shifts Capture started before web fonts loaded, or fonts are unavailable in the container Await document.fonts.ready, verify font URLs and install the required font files.
Charts or images are blank Rendering is asynchronous or external resources are blocked Wait for an application-ready selector, check network access and use authenticated cookies or headers.
Navigation times out The page never reaches the selected network-idle state Use a bounded domcontentloaded wait followed by a specific ready selector, and investigate failing requests.
Browser will not launch in a container Missing binary, libraries or sandbox permissions Install the package’s compatible browser and system dependencies. Change sandbox settings only according to your container’s security policy.
Content is clipped or breaks badly Conflicting API margins, @page rules or non-breaking layout elements Choose one page-size strategy, set margins deliberately and add print break rules for tables, flex items and images.
Requests become slow or exhaust memory A browser is launched per request or too many pages run concurrently Reuse a browser, cap concurrency, close contexts in finally and measure with realistic fixtures.
PDFKit output does not match an HTML mockup PDFKit has no browser CSS engine Either implement the layout explicitly in PDFKit or switch to Puppeteer/Playwright for HTML fidelity.

A practical selection checklist

  1. Start with the source. If the source is an existing web page, choose Puppeteer or Playwright. If it is a fixed specification, consider PDFKit.
  2. List browser-only requirements. Include JavaScript charts, web fonts, authentication, responsive breakpoints and client-side data.
  3. Define print behavior. Decide media type, page size, margins, backgrounds, headers, footers and page-break rules before coding.
  4. Prototype the largest document. Test long tables, missing assets, slow fonts and multiple pages rather than a small happy-path page.
  5. Plan operations. Select a browser image, font set, sandbox policy, worker lifetime, concurrency limit and timeout.
  6. Automate visual checks. Keep fixtures and review PDF differences whenever the browser or package changes.

FAQ

Should PDF generation run inside a normal web request?

For short, predictable documents it can. For large pages or uncertain third-party resources, queue the job and return a status URL so request timeouts do not determine document reliability.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What makes a useful visual-regression fixture?

Use a page with web fonts, a chart, a long table that crosses pages, a forced page break and an image that loads over the network. That combination exposes most changes in waiting, pagination and font metrics.

Can a PDF reproduce an interactive workflow?

It captures the final rendered state. Script required clicks, selections or expansion panels before calling the PDF API; interactivity that exists only in the browser will not continue running inside a static PDF.

Frequently Asked Questions

Should PDF generation run inside a normal web request?

For short, predictable documents it can. For large pages or uncertain third-party resources, queue the job and return a status URL so request timeouts do not determine document reliability.

What makes a useful visual-regression fixture?

Use a page with web fonts, a chart, a long table that crosses pages, a forced page break and an image that loads over the network. That combination exposes most changes in waiting, pagination and font metrics.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can a PDF reproduce an interactive workflow?

It captures the final rendered state. Script required clicks, selections or expansion panels before calling the PDF API; interactivity that exists only in the browser will not continue running inside a static PDF.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.