HTML to PDF in JS A Guide to 3 Key Methods
Back to Blog

HTML to PDF in JS A Guide to 3 Key Methods

17 min read

You probably landed here after trying the obvious thing first.

You have HTML on screen, a user wants a PDF, and JavaScript seems like it should make that easy. Then significant problems show up. The invoice looks fine in Chrome but breaks in the export. Your dashboard cards wrap differently in the PDF. A cookie banner sneaks into the first page. Text becomes blurry. Memory spikes. Someone files a bug because page two starts halfway through a chart.

That is the true nature of html to pdf in js work. It is not just about finding a library. It is about choosing where rendering happens, what quality you can tolerate, and how much operational pain you are willing to own.

Why Generating PDFs from HTML is Tricky

HTML and PDF solve different problems.

A browser lays out content for an interactive, fluid viewport. A PDF expects fixed pages, stable dimensions, and predictable pagination. Browser content can keep changing after load because fonts, images, and app state arrive asynchronously. A PDF wants a finished document.

That mismatch is why a simple browser print flow rarely holds up in production. It can be acceptable for internal tools, but once users depend on consistent output, you need programmatic control over timing, page size, margins, hidden UI, and page breaks.

Three broad approaches exist:

  • Client-side rendering inside the user’s browser with tools like jsPDF, html2canvas, or html2pdf.js
  • Server-side rendering with a headless browser such as Puppeteer
  • Dedicated API rendering, where a service handles the browser automation and returns the PDF

Each path makes a different trade.

Client-side is the easiest to start with. You add a package, target a DOM node, and trigger a download. It feels lightweight because you avoid backend work. The catch is that many client-side tools are not producing a true document from layout primitives. They are taking a screenshot-like path through a canvas and embedding that image in a PDF.

Server-side rendering is heavier, but it uses a real browser engine to print the page. That usually means far better CSS support, cleaner pagination, and fewer surprises when the page uses modern layout techniques.

API-based rendering takes the same quality-oriented idea and pushes the infrastructure burden elsewhere. You call an endpoint instead of managing browser workers, queues, retries, and cleanup yourself.

The hard part is rarely “can JavaScript generate a PDF?” The hard part is “which rendering model breaks least for this document?”

If you treat every html to pdf in js problem as the same problem, you will pick the wrong tool. A receipt export button and a compliance-grade archive do not belong on the same implementation path.

Client-Side PDF Generation with jsPDF

A familiar project starts this way. Product wants an Export PDF button by Friday, the document looks simple enough, and nobody wants to stand up backend rendering for a feature that might stay small. That is exactly why jsPDF gets picked so often.

It is a practical browser-side option, and for the right job it works. jsPDF has been around for years, the API is easy to wire into an app, and its .html() method can handle basic layouts without much setup. Nutrient’s html to pdf in JavaScript analysis also points out the limitation many teams hit later. Fidelity is decent on simple pages and drops once the layout depends heavily on flexbox, grid, or other modern CSS.

A hand-drawn sketch showing a web browser converting HTML code into a downloadable PDF document.

A basic jsPDF example

If your page section is simple, this works as a starting point:

import { jsPDF } from "jspdf";

async function downloadPdf() {
  const element = document.getElementById("report");

  const pdf = new jsPDF({
    orientation: "portrait",
    unit: "mm",
    format: "a4"
  });

  await pdf.html(element, {
    callback: function (doc) {
      doc.save("report.pdf");
    },
    x: 10,
    y: 10,
    margin: [10, 10, 10, 10],
    html2canvas: {
      scale: 2,
      useCORS: true
    }
  });
}

document.getElementById("export-btn").addEventListener("click", downloadPdf);

The appeal is obvious.

Everything stays in the browser. You can attach export directly to a React, Vue, or plain JavaScript view, skip server infrastructure, and return a file to the user immediately. For invoices, summaries, or internal reports with controlled formatting, that convenience is hard to beat.

What jsPDF is rendering

The trade-off starts with the rendering model.

In many real implementations, jsPDF’s HTML workflow depends on html2canvas to capture the DOM into a canvas first. That output is then embedded into the PDF, often as image data rather than a clean print representation built from browser layout primitives. If you want a broader comparison of browser-side and Node-based approaches, this guide to HTML to PDF in Node.js is a useful reference point.

That screenshot-style path creates predictable limitations:

  • Text can look softer than browser print output, especially when users zoom in
  • Selecting or searching text may not behave cleanly
  • Complex CSS support becomes inconsistent, especially with grid, transforms, layered effects, and some font rendering cases
  • File size tends to grow when the document is mostly image content

This is the hidden cost of client-side convenience. Setup is light, but fidelity has a ceiling.

Where jsPDF makes sense

jsPDF is a reasonable choice when the document is small, user-triggered, and not compliance-sensitive.

It fits well when you need:

  • Simple structure such as single-column reports, receipts, or basic invoice layouts
  • Immediate download in the browser without queueing work on a server
  • Loose visual tolerance, where close enough is acceptable
  • No backend PDF infrastructure, whether for speed, privacy, or architecture constraints

I still use it for internal tools and low-risk exports. It keeps the feature local to the frontend and avoids operational work on backend jobs, storage, retries, and deployment on cloud servers.

Where maintenance starts getting expensive

Problems usually appear after the first successful demo.

The page gains a web font. A chart library finishes rendering after the export call. Someone adds a responsive two-column layout that looks fine in the app but collapses awkwardly in the PDF. Marketing uploads images from another domain. Then the team is debugging rendering timing, CORS rules, scaling, and page breaks inside the browser.

The common failure points are familiar:

  • Cross-origin assets can disappear unless image hosting and CORS are configured correctly
  • High canvas scale values can spike memory use and freeze weaker devices
  • Fonts, charts, and async data may render after the PDF capture starts
  • Long documents often need manual page splitting, which is brittle once content changes

Those issues are manageable on a narrow template. They become recurring maintenance work once the PDF needs to mirror a real app screen.

Client-side HTML to PDF works best when convenience matters more than exact output.

The decision

The question is not whether jsPDF is good or bad. The question is whether your document can tolerate the screenshot-style rendering model.

If the answer is yes, jsPDF stays fast to ship and easy to keep on the frontend. If the answer is no, the time you save at the start often gets paid back in bug fixes, edge cases, and layout exceptions. That is why many teams begin with client-side generation for simple exports, then switch once PDF quality becomes part of the product requirement rather than a nice extra.

Server-Side Rendering for Perfect Fidelity

When the output has to look right, many teams end up with a headless browser.

Puppeteer is the common choice in Node.js because it drives a real Chrome or Chromium instance. That changes the problem completely. You are no longer turning a DOM snapshot into an image. You are asking a browser engine to render the page and print it as a PDF.

A conceptual diagram showing how the Puppeteer tool converts HTML content via a headless browser into a high fidelity PDF file.

A practical Puppeteer flow

A minimal implementation looks like this:

const puppeteer = require("puppeteer");

async function generatePdf(url) {
  const browser = await puppeteer.launch({ headless: true });

  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: "networkidle0" });

    const pdf = await page.pdf({
      format: "A4",
      printBackground: true
    });

    return pdf;
  } finally {
    await browser.close();
  }
}

This is usually the first approach that feels trustworthy.

The app page renders with the same engine your users already test against. CSS support is far stronger. Print styles work naturally. Pagination behaves like a browser print task rather than a stitched image export.

Why fidelity improves so much

The practical difference is rendering depth.

For server-side Node.js use, Puppeteer achieves 95%+ visual accuracy with complex CSS, but it takes 2-4 seconds per document and uses 100-200MB RAM per browser instance according to the html2pdf.js documentation discussion of Puppeteer trade-offs.

Those numbers line up with what teams feel in production. The output gets better, but the infrastructure becomes real work.

If you host this yourself, choose machine capacity with room for browser processes, not just your app server. That is where a setup like cloud servers becomes relevant, because a PDF worker has different resource pressure than a typical stateless API container.

What goes wrong on the server

The common failure mode is not rendering quality. It is operations.

You now own:

  • Browser lifecycle management
  • Concurrency limits
  • RAM spikes under load
  • Crash recovery
  • Timeout handling
  • Temporary authentication flows
  • OS-level dependencies in containers

And you must close browser instances every time. If you skip await browser.close(), long-running services eventually punish you with memory leaks and stuck workers.

For a deeper Node-focused walkthrough, the ScreenshotEngine team has a useful guide on HTML to PDF in Node.js.

A few patterns that make server-side rendering survivable

Wait for the page to be done

Dynamic apps rarely settle the instant DOMContentLoaded fires.

Use navigation and readiness checks that reflect how your app loads. If charts or client-side data fetches arrive late, gate PDF creation on those signals rather than pure URL load.

Add print-specific CSS

Server-side rendering gets much better when you stop trying to print the raw app shell.

Hide navigation, sticky elements, chat widgets, and buttons. Adjust spacing for fixed pages. If your app has a dashboard layout, add a print stylesheet instead of hoping the screen layout behaves.

A short demonstration is useful here:

Separate rendering from request handling

Do not tie heavy browser work directly to your primary web thread if you expect volume. Use workers or a job queue when exports become frequent.

That one design choice often determines whether Puppeteer feels solid or fragile.

Server-side browser rendering is usually the right answer when the PDF is part of the product, not just a convenience feature.

The downside is clear. You are running a browser farm, even if it starts as one tiny worker process.

Choosing Your HTML to PDF Strategy

The right choice depends less on language and more on constraints.

A useful way to evaluate html to pdf in js options is to compare them across four axes: fidelity, speed, maintenance, and implementation shape.

Infographic

A practical comparison

Strategy Best fit Main strength Main weakness
Client-side libraries Quick exports from simple pages Fast to add inside a frontend app Fidelity drops fast as layouts get richer
Server-side rendering Reports, invoices, archives, branded docs High visual consistency Browser infrastructure is heavy
Dedicated API Teams that want output quality without owning browser workers Simple integration with managed rendering External service dependency

Start with the document, not the tool

A few questions usually make the decision obvious.

If the PDF is just a convenience download for user-generated content, client-side is reasonable. The implementation is close to the UI, and users get immediate feedback.

If the PDF has legal, financial, or archival importance, use a browser-based print engine. That usually means server-side rendering or an API built around that model.

If your team already manages queues, workers, and memory-heavy services well, Puppeteer fits naturally. If not, do not underestimate the long-term cost of “just running Chrome on the server.”

The lowest-friction decision rules

Pick client-side if

The document is simple, users trigger it manually, and minor layout drift is acceptable.

This is the path for internal tools, lightweight receipts, and rough exports where speed of implementation matters more than exactness.

Pick server-side if

You need stable rendering and you want direct control over the browser environment, authentication flow, and output process.

This is the path for product teams that already have backend maturity and can support browser workers responsibly.

Pick an API if

You want browser-grade output without operating the rendering stack yourself.

A dedicated service can also simplify related tasks. For example, if you need PDF export alongside page screenshots or visual capture workflows, a single API surface is cleaner than stitching multiple tools together. If you want to compare that route, ScreenshotEngine has additional reading on its HTML to PDF API approach.

Teams frequently do not fail because they chose a bad library. They fail because they chose a rendering model that did not match the document’s importance.

That is the core decision framework. Do not ask which package is popular first. Ask how wrong the PDF is allowed to be, and who will maintain the failures when it is.

The API Workflow The Easiest Path to Perfect PDFs

There is a middle path between browser-side shortcuts and running Puppeteer infrastructure yourself.

A dedicated rendering API gives you server-side style output without requiring your team to manage Chromium instances, memory pressure, or worker orchestration. For many product teams, that is the most practical shape of html to pdf in js once exports move beyond trivial documents.

Screenshot from https://screenshotengine.com/docs

What the API model changes

Instead of launching a browser in your app, you send a request describing what should be rendered.

That removes a long list of backend concerns:

  • browser installation and updates
  • worker cleanup
  • queue design
  • memory spikes under concurrency
  • retry logic around failed page sessions

It also centralizes rendering for teams that need more than PDFs. Some workflows need screenshots for previews, scrolling videos for demos, and PDF export for archiving. A single capture API can cover all three.

A simple request pattern

With a PDF generation API, the request shape is usually straightforward. In Node.js, it can look like this:

const axios = require("axios");
const fs = require("fs");

async function generatePdfFromApi() {
  const response = await axios.get("https://api.screenshotengine.com/your-request", {
    responseType: "arraybuffer"
  });

  fs.writeFileSync("output.pdf", response.data);
}

The implementation details depend on the provider, but the appeal is consistent. Your application asks for a rendered output and receives a file. No browser lifecycle code sits in your service.

If you want a provider-specific walkthrough, the ScreenshotEngine article on a PDF generation API shows the request style and integration options.

When this is the right move

This approach makes sense when your team wants reliable output but does not want PDF rendering to become an infrastructure project.

That is especially true when documents are generated from full web pages, authenticated dashboards, or marketing pages that need clean captures. Services in this category can also help with common annoyances like blocking intrusive overlays before rendering. That matters more than generally expected, because cookie notices and popups are a common source of ugly exports.

ScreenshotEngine is one example of this model. It offers an API for image capture, scrolling video, and PDF output through the same interface. For teams already automating visual capture, that keeps integration simpler than mixing a screenshot tool with a separate PDF stack.

The trade-off is different, not absent

The API route is not magic.

You still need to think about readiness conditions, authentication, and print styling. You also introduce an external dependency into a workflow that may be business-critical.

But the maintenance profile changes in your favor. Instead of spending engineering time on browser operations, you spend it on document quality and integration logic. That is usually a better use of time.

If your PDF pipeline keeps drifting from “feature” into “mini platform,” an API is often the cleanest correction.

Advanced Tips for Professional PDF Output

A PDF can pass every local test and still fail in production because the page was captured half a second too early, a web font swapped late, or a responsive component expanded in ways no one planned for print. That is why the last 10 percent of PDF quality usually comes from document preparation, not from the export call itself.

Write print-specific CSS

Treat PDF output as its own surface.

Screen layouts optimize for interaction. PDFs optimize for reading, page flow, and repeatable rendering. Those goals overlap less than many teams expect. Hide navigation, sticky headers, hover controls, chat widgets, and anything else that burns space without helping the reader. Add page-break rules where sections, charts, or signatures need to stay together. If a component only makes sense on screen, build a print variant instead of forcing it into the export.

Accessibility belongs in that same review. Teams that care about readable structure, tagging, and usable exported documents should review guidance on creating inclusive PDFs.

Wait for dynamic content

This is the mistake I see most often.

Modern apps reach a visible state long before they reach a stable render state. The route is mounted, but the font file is still loading, the chart is still animating, and the last API response has not populated the table. Client-side capture is especially sensitive to this because it freezes whatever the browser has at that exact moment. Server-side renderers and APIs are more forgiving, but they still need a clear readiness signal.

Practical fixes include:

  • Wait for data markers. Trigger export only after the final component state is present.
  • Load fonts before capture. Late font swaps cause line wrapping and page drift.
  • Disable chart animations. Animated canvases and SVG transitions create inconsistent exports.
  • Use explicit ready hooks. A window.__PDF_READY__ = true style signal is often more reliable than waiting an arbitrary number of milliseconds.

Design for pages, not for infinite scroll

Long web pages rarely become good PDFs without edits.

Infinite scroll, oversized cards, sticky sidebars, and giant hero sections all work against page-based output. Tables need header repetition and controlled row breaks. Dense reports need predictable margins and type sizes. Marketing pages often need a separate print template because the screen version is built for persuasion, not pagination.

This is also where the client-side versus server-side trade-off becomes obvious. Client-side tools are convenient when the document is simple and the browser already has the data. They get expensive to maintain once page breaking, font consistency, and repeatable layout start to matter. Server-side rendering and PDF APIs reduce that fidelity risk, but you pay in infrastructure, vendor dependency, or per-document cost.

The teams with the fewest PDF surprises usually do two things well. They prepare HTML for print, and they choose a rendering path that matches the document's quality bar.

If you need browser-rendered PDF output without building and operating the rendering stack yourself, ScreenshotEngine is a practical option to evaluate. It provides a clean API for PDF export as well as screenshots and scrolling videos, which is useful when a single workflow needs more than one type of capture.