Blog/Compare

html2pdf.js vs a server HTML to PDF API

html2pdf.js is a browser library. It asks html2canvas to paint a DOM node onto a canvas, then asks jsPDF to embed that bitmap in a PDF.

Compare··6 min read

html2pdf.js is a browser library. It asks html2canvas to paint a DOM node onto a canvas, then asks jsPDF to embed that bitmap in a PDF.

A server API is a different machine. Chromium loads HTML and prints it.

Searchers want selectable text, smaller files, or they refuse to put a vendor key in a front-end bundle.

Product: https://relaypdf.com/html-to-pdf.

In-process Chromium compare: https://relaypdf.com/compare/puppeteer Sibling post: https://relaypdf.com/blog/playwright-pdf-vs-api.

Request fields: https://relaypdf.com/docs/pdf.

What the client library actually does

The project README (eKoopmans repo, MIT, Erik Koopmans) is explicit. It converts a webpage or element to PDF entirely client-side using two dependencies. It will not run in Node. The worker chain is from, toContainer, toCanvas, toImg, toPdf, save. Default export uses JPEG at quality 0.95. You can pass option bags through to each dependency. Page-break modes are avoid-all, css (break-before / break-after / break-inside, limited values), and legacy (a dedicated page-break class). enableLinks overlays hyperlinks on top of anchors after the raster step.

Known issue 4 on that README is the quality story: the library renders all content into an image, then places that image into a PDF. Text is not selectable or searchable. File sizes grow because you are shipping pixels. Known issue 6 is canvas geometry: HTML canvases have a maximum height and width; anything larger can fail to render, and large documents can come out completely blank. Known issue 1 is that the canvas painter is the renderer, and if it misses a CSS feature the wrapper cannot invent one. Known issue 3 is that the library resizes the root element to fit a PDF page, which reflows the layout you thought you captured.

What jsPDF is if you skip the wrapper

jsPDF (parallax/jsPDF) is a client-side PDF writer. You call text, lines, rectangles, and images. Those text operators are real PDF text: selectable, searchable, usually smaller than a full-page JPEG. That is a different product. You are not converting HTML. You are drawing a document in code. If your invoice is a table of numbers and a logo, jsPDF is enough. If your invoice is a React tree with flex, web fonts, and a chart library, you will either hand-port the layout or fall back to a raster path.

jsPDF also ships HTML helpers in some builds. Those helpers still have to get pixels or a limited CSS subset from the browser. Do not treat that helper as Chromium print. The stack is still the user tab, the same origin policy, and whatever the canvas can see.

Quality: canvas versus print

A Chromium print PDF keeps text as text. Zoom stays sharp. Find-in-document works. Screen readers have a fighting chance. Headers and footers can be HTML templates with pageNumber and totalPages, not a second screenshot band. RelayPDF options use the Chromium names: format, printBackground (default true), waitUntil (default networkidle0), timeout max 60000 ms, headerTemplate and footerTemplate. See https://relaypdf.com/docs/options. Formats include letter, legal, tabloid, ledger, and A0 through A6.

A canvas PDF is a photograph of a tab. It looks like the screen at one scale. Raise the painter scale and you pay memory. Lower JPEG quality and you pay artifacts. Page splits cut through the tall bitmap; avoid-all helps and then explodes page count on a dense table. Flex, grid, sticky, webfonts, and SVG filters are best-effort. That is not a rating. It is the project own caveat: if the canvas is wrong, the PDF is wrong.

Cross-origin assets and what a canvas can see

The painter reads pixels from the document. Images from another origin without the right headers taint the canvas. A tainted canvas will not export. The usual knobs are useCORS and proxy hacks. Fonts from another origin have the same class of problem. Your export button now depends on CDN headers you do not control.

A server printer does not inherit the user tab. You send HTML as UTF-8 JSON (not Base64) on POST /v1/pdf, or you send a public http(s) URL. RelayPDF rejects private, loopback, and metadata hosts on url. If the logo is private, inline it or host it where the renderer is allowed to fetch. extraHTTPHeaders and cookies exist for url fetches (docs/options). That is still not the user logged-in session, and you should not pretend it is.

Secrets in the browser

Anything the client library can print, the user can already see: DevTools, Save as, a screenshot. That is fine for a receipt of their own order. It is not fine for a staff invoice that includes another tenant line items, a webhook secret, or a vendor key you planned to hide behind a minifier. If the SPA calls a PDF host directly, the bearer token is in the bundle. Put the key on a server. Assemble HTML there. Return application/pdf.

Client-side generation also means you cannot enforce a single layout for legal or finance. Each browser, zoom level, and extension theme can change the canvas. A server Chromium run with waitUntil and a timeout is still not a typesetter, but it is one engine, one paper size, one printBackground default.

Side by side

CapabilityClient librariesServer Chromium (RelayPDF)
Where it runsBrowser only. README: will not run in Node.Your backend calls POST https://api.relaypdf.com/v1/pdf.
What the PDF containsBitmap pages, or real text if you draw with jsPDF operators.Chromium print: vector text, embedded fonts, print CSS.
HTML / CSSCanvas subset. Root may reflow to page width.Browser HTML/CSS/JS. printBackground default true.
Cross-origin assetsTab origin plus canvas taint. useCORS / proxies.HTML you send, or a public URL. Private hosts rejected.
SecretsEverything is in the client. Do not ship vendor keys.Key in env. HTML assembled server-side.
PaginationPage-break modes plus canvas max size (blank risk).Chromium pages. header/footer HTML. timeout max 60s.
Scale / billingUser CPU and RAM. No vendor bill.Prepaid wallet. HTML/URL/Markdown/template PDF 0.015 USD. render_failed not billed.

The API call if you leave the tab

POST /v1/pdf takes exactly one of html, url, markdown, or templateId. Response is binary by default, or url (24-hour download) or async plus optional callbackUrl. Node helper from the official quickstart:

import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({ apiKey: process.env.RELAYPDF_API_KEY });
const pdf = await client.pdf.fromHtml(html, { filename: "invoice.pdf" });
await pdf.save("invoice.pdf");

Placeholder key. Keep it on the server, not in a browser bundle.

Client-side counterpart, from the library README, so the contrast is not abstract:

var el = document.getElementById("element-to-print");
html2pdf().set({
  margin: 1, filename: "myfile.pdf",
  image: { type: "jpeg", quality: 0.98 },
  html2canvas: { scale: 2 },
  jsPDF: { unit: "in", format: "letter", orientation: "portrait" }

}).from(el).save();

When to use which

Stay on the client libraries when

  • The document is the user own view and they already have the pixels.
  • You cannot add a backend, and quality is looks like the screen.
  • You will draw the PDF with jsPDF operators instead of converting HTML.
  • The file is a one-page receipt, not a 40-page report that will hit canvas limits.

Move to a server API when

  • You need selectable text, find-in-PDF, or print CSS that matches Chrome.
  • Images and fonts are cross-origin and you are tired of tainted canvases.
  • The HTML includes data or keys that must not ship to the browser.
  • You want one POST for HTML, a public URL, Markdown, or a Handlebars templateId.

Limits

RelayPDF is not a canvas polyfill. URL fetch is public hosts only. Chromium timeout max is 60 seconds; render_failed is not billed. Office files go to POST /v1/convert, merge to POST /v1/pdf/merge. This page does not claim OCR, e-sign, PDF/A, or HIPAA. Pricing from the live card used across this series: prepaid wallet, credit never expires, HTML/URL/Markdown or template PDF 0.015 USD, 5 USD signup credit, failures never billed (https://relaypdf.com/pricing). The browser library remains the right tool when you truly need zero servers.

Four steps if you leave the canvas

  1. Keep the client library on screens that are already download what you see.
  2. Move invoices, contracts, and anything with third-party data to a server that holds the vendor key.
  3. Send the same HTML you already render, as UTF-8 JSON, with format and waitUntil set. Compare text select and file size, not a slide.
  4. If you would rather run a local Chromium driver, read the Playwright sibling post. The API does not replace a driver you still want.

CTA is the product, not a brand swap: https://relaypdf.com/html-to-pdf.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.