pdf-lib, jsPDF, and PDFKit are the three most widely used JavaScript PDF libraries, and they do fundamentally different things. pdf-lib reads and modifies existing PDFs. jsPDF draws PDFs in the browser using a canvas-like API. PDFKit writes PDFs from scratch on a Node.js server using a streaming drawing API. Pick the wrong one and you will fight the library for weeks; pick the right one and the task takes an afternoon.
This guide is an honest developer comparison. It covers what each library is good at, what it cannot do, the same "hello world" code in all three, bundle sizes, runtime environments, and a decision table. At the end, it names the case where none of the three is the right tool: generating a PDF from HTML and CSS.
What does each library actually do?
Each library targets a different slice of the PDF problem space. pdf-lib manipulates existing PDF object graphs. jsPDF builds new PDFs from primitive drawing calls in the browser. PDFKit builds new PDFs from primitive drawing calls on a Node.js server with a streaming API. None of them parses HTML or CSS, which is the most common misconception.
| Library | Primary use | Runtime | Starting point | License |
|---|---|---|---|---|
| pdf-lib | Edit and combine existing PDFs | Node and browser | Existing PDF bytes | MIT |
| jsPDF | Generate new PDFs in the browser | Browser (Node with caveats) | Blank document | MIT |
| PDFKit | Generate new PDFs on a server | Node.js (browser via bundle) | Blank document | MIT |
The key distinction: pdf-lib is a manipulation library, jsPDF and PDFKit are generation libraries. If the sentence describing your task contains the word "modify", "merge", "split", "fill", "add", or "stamp", pdf-lib is almost certainly the answer. If the sentence contains "generate from scratch" and runs in a browser tab, jsPDF. If it runs in a Node server, PDFKit or an HTML-to-PDF approach.
pdf-lib: edit existing PDFs
pdf-lib is the only library of the three that can read an existing PDF and produce a modified version. It parses the PDF object graph, exposes pages as manipulable objects, and writes the result back out. Because it streams objects rather than re-rendering them, operations like merge and split are extremely fast. A 100-page merge takes well under a second.
// pdf-lib: merge two PDFs and add a watermark to each page
import { PDFDocument, rgb, StandardFonts, degrees } from 'pdf-lib';
import fs from 'node:fs/promises';
const merged = await PDFDocument.create();
const font = await merged.embedFont(StandardFonts.HelveticaBold);
for (const file of ['a.pdf', 'b.pdf']) {
const bytes = await fs.readFile(file);
const src = await PDFDocument.load(bytes);
const pages = await merged.copyPages(src, src.getPageIndices());
for (const page of pages) {
const { width, height } = page.getSize();
page.drawText('DRAFT', {
x: width / 2 - 60,
y: height / 2,
size: 72,
font,
color: rgb(0.8, 0.1, 0.1),
opacity: 0.2,
rotate: degrees(30),
});
merged.addPage(page);
}
}
await fs.writeFile('out.pdf', await merged.save());What pdf-lib is good at: merging PDFs, splitting pages, rotating, cropping, filling AcroForm fields, adding page numbers or watermarks to existing documents, and extracting text metadata. The API is functional and flat: every operation returns either a new document or mutates an existing one.
What pdf-lib cannot do: render HTML, lay out text across pages (you place text at absolute coordinates), load fonts by name (fonts must be embedded as raw bytes), or flatten complex form state. It is a manipulation tool, not a layout engine.
pdf-lib is the library behind most of the free browser-based PDF tools you see on the web, including most of the tools on PDF4.dev itself. If your task is "do something to an existing PDF", start here before reaching for a heavier dependency.
jsPDF: client-side PDF generation
jsPDF runs entirely in the browser and produces a PDF as a Blob or data URI that you hand to the user. It ships a canvas-like drawing API (doc.text(), doc.line(), doc.rect()), optional font embedding, and an extension called autoTable for data grids. The entire flow never touches your server, which is attractive for privacy-sensitive apps.
// jsPDF: generate an invoice entirely in the browser
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.text('Invoice #1024', 20, 25);
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text('Acme Corp', 20, 35);
doc.text('123 Startup Lane', 20, 40);
autoTable(doc, {
startY: 55,
head: [['Description', 'Qty', 'Price', 'Total']],
body: [
['Consulting hours', '10', '$150', '$1,500'],
['Software license', '1', '$500', '$500'],
],
theme: 'striped',
});
doc.save('invoice-1024.pdf');What jsPDF is good at: simple generated documents (receipts, single-page reports, printable forms) in a browser context, combining with html2canvas to screenshot a DOM node into a PDF, and distributing as a single-script bundle. It has no server dependency, so it works offline.
What jsPDF is not good at: complex CSS layouts. The html2canvas approach rasterizes the DOM into a bitmap image, so the resulting PDF is a giant image with no selectable text, no real typography, and file sizes that balloon quickly. Font support is limited to the built-in standard fonts unless you load TTF files manually. Vector text from actual HTML is not possible.
PDFKit: streaming server-side PDFs
PDFKit was originally written for Node.js and streams PDF bytes as they are generated. It uses a chained drawing API with cursor positioning, embeds fonts from TTF or OTF files, supports SVG paths, and can produce long documents without holding the full file in memory. It is the oldest of the three (first release 2012) and the most feature-complete as a pure generation library.
// PDFKit: stream a multi-page report to a file
import PDFDocument from 'pdfkit';
import fs from 'node:fs';
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('report.pdf'));
doc.font('fonts/Inter-Bold.ttf')
.fontSize(24)
.text('Quarterly Report', { align: 'center' });
doc.moveDown()
.font('fonts/Inter-Regular.ttf')
.fontSize(12)
.text('This report covers Q1 2026 financial metrics.');
// Draw a simple table by hand (no built-in table helper)
const rows = [
['Metric', 'Q1 2026', 'Q4 2025'],
['Revenue', '$1.2M', '$0.9M'],
['Net profit', '$220K', '$180K'],
];
let y = 200;
rows.forEach((row, i) => {
row.forEach((cell, j) => doc.text(cell, 50 + j * 150, y));
y += 20;
if (i === 0) doc.moveTo(50, y - 5).lineTo(500, y - 5).stroke();
});
doc.addPage().text('Page 2: appendix', { align: 'center' });
doc.end();What PDFKit is good at: long streaming documents (reports, logs, data dumps), fine control over typography with real TTF embedding, SVG graphics, and low memory footprint because it writes to a stream instead of buffering. It is the library you reach for when you want to generate a 500-page PDF on a Node server without running out of memory.
What PDFKit is not good at: ergonomic APIs. You write pagination by hand, you compute table column widths by hand, you track cursor position by hand. There is no reflow, no flexbox, no grid. Complex layouts take hundreds of lines of positioning code.
Feature matrix
The table below gives a blunt summary of capabilities. "Yes" means first-class support. "Partial" means it works but with limitations. "No" means the library cannot do it at all.
| Feature | pdf-lib | jsPDF | PDFKit |
|---|---|---|---|
| Merge existing PDFs | Yes | No | No |
| Split existing PDFs | Yes | No | No |
| Fill AcroForm fields | Yes | Partial | No |
| Add watermark to existing PDF | Yes | No | No |
| Generate PDF from scratch | Partial | Yes | Yes |
| HTML to PDF | No | Via html2canvas (rasterized) | No |
| Real text layout across pages | No | Limited | Yes |
| Embed TTF or OTF fonts | Yes | Yes | Yes |
| Stream output (low memory) | No | No | Yes |
| Runs in browser | Yes | Yes | With bundle (~1.5 MB) |
| Runs in Node.js | Yes | Partial | Yes |
| Form creation | Yes | Partial | Partial |
| Images (PNG, JPG) | Yes | Yes | Yes |
| SVG paths | Partial | Partial | Yes |
| Encryption | No | Partial | Partial |
Bundle size and runtime environment
Bundle size matters when you ship to the browser. The numbers below are for the minified plus gzipped bundle at the time of writing, including the library's own dependencies.
| Library | Bundle (browser) | Node.js | Edge runtimes (Cloudflare Workers, Vercel Edge) |
|---|---|---|---|
| pdf-lib | 360 KB | Yes | Yes (pure JS, no native deps) |
| jsPDF | 410 KB (plus 95 KB for autoTable) | Partial | Partial (no canvas) |
| PDFKit | 1.5 MB (standalone bundle) | Yes | No (uses fs and stream) |
pdf-lib has the smallest browser footprint and the widest runtime coverage. It runs in Cloudflare Workers, Vercel Edge, Deno, Bun, and the browser without modification. PDFKit is the heaviest because it bundles its own font subsetter (Fontkit) and image decoders. jsPDF sits in the middle but has trouble outside the browser.
PDFKit does not run on Cloudflare Workers or Vercel Edge. It imports Node built-ins (fs, stream, zlib) that are not available in V8 isolate runtimes. If your deployment target is an edge function, pdf-lib is your only option among the three.
"I want X, pick Y" decision table
This is the fastest way to pick a library. Find your task on the left, read the recommendation on the right.
| I want to... | Use |
|---|---|
| Merge 10 uploaded PDFs into one | pdf-lib |
| Split a 50-page PDF into individual pages | pdf-lib |
| Add a watermark to an existing contract | pdf-lib |
| Fill a government form PDF with data | pdf-lib |
| Generate a receipt in the browser without a backend | jsPDF |
| Export a chart to PDF from a React app | jsPDF + html2canvas (rasterized) |
| Build a 500-page streaming log export on a Node server | PDFKit |
| Generate a branded invoice from an HTML/CSS design | None of the above: use Chromium or a PDF API |
| Render a React component as a pixel-perfect PDF | None of the above: use Playwright or a PDF API |
| Build a resume with Tailwind-styled output | None of the above: use Chromium or a PDF API |
The last three rows are the honest answer most teams do not want to hear: if your design lives in HTML and CSS, no JavaScript PDF library will render it faithfully. The PDF spec and the HTML spec are different enough that re-implementing a CSS layout engine in PDFKit is a multi-year project nobody has shipped.
The missing option: HTML to PDF via Chromium
When your template is HTML, the only correct renderer is a browser. Chromium already has a paged-media engine (CSS Paged Media Level 3) and it renders the same way in print preview and in generated PDFs. Playwright exposes it with page.pdf(). Puppeteer exposes it the same way. PDF4.dev wraps a pool of warm Chromium instances behind an HTTP API so you do not have to run them yourself.
// PDF4.dev: HTML to PDF in one API call
await fetch('https://pdf4.dev/api/v1/render', {
method: 'POST',
headers: {
Authorization: 'Bearer p4_live_...',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: '<h1>Hello from HTML</h1><p>Styled with CSS, rendered by Chromium.</p>',
format: { preset: 'a4', margins: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' } },
}),
});Why this matters for the comparison: most teams reaching for pdf-lib, jsPDF, or PDFKit were actually looking for HTML-to-PDF and did not know the browser was the right tool. If that describes your task, stop wrestling with drawing primitives and point Chromium at your HTML.
Honest tradeoffs
No library is perfect. Here are the real issues you hit in production.
pdf-lib. The API is flat and functional, which is great for merge and split but awkward for anything involving layout. Text flow is absent: you place strings at coordinates. Embedding a TTF font costs ~200 ms on first use because the subsetter has to scan the font tables. Memory usage is proportional to the input document, so a 500 MB PDF will take 500 MB of heap to load.
jsPDF. The html2canvas integration is a trap. It rasterizes the DOM into a large image, which kills text selection and accessibility, and produces files five to ten times larger than a real vector PDF. Font support outside the 14 standard PDF fonts requires you to load the TTF file as base64 in your bundle. In Node.js, the library technically runs but several features silently no-op without a browser DOM.
PDFKit. The streaming API is great for throughput but terrible for debuggability. You cannot inspect the document in memory because it is already on the wire. Typography is powerful but verbose: every text block takes four or five .text() calls. The browser bundle is huge because Fontkit ships an entire OpenType subsetter, which you need for any non-Latin script.
Try the HTML to PDF approach
If you were reaching for a JS PDF library because your design is in HTML, the browser is the right renderer and PDF4.dev is the lowest-friction way to run one. Send HTML, get a PDF, pay for what you use.
Try the PDF merge tool (powered by pdf-lib)Try it freeFAQ
Which JavaScript PDF library should I pick?
Pick pdf-lib if you need to edit or combine existing PDFs (merge, split, fill forms, add watermarks). Pick jsPDF if you need to generate a PDF entirely in the browser without a server round-trip. Pick PDFKit if you need a streaming Node.js server that produces PDFs from scratch using drawing primitives. If you are starting from HTML, none of these are the right tool: use a headless browser or an HTML-to-PDF API instead.
Can pdf-lib render HTML to PDF?
No. pdf-lib operates on existing PDFs or builds pages from primitives (text, rectangles, images). It has no HTML parser and no CSS engine. For HTML-to-PDF you need Chromium (Playwright, Puppeteer) or an API like PDF4.dev.
Does jsPDF work in Node.js?
Yes, jsPDF runs in Node with some caveats. The canvas-based features (html2canvas integration) need a DOM polyfill and do not work well server-side. Pure drawing commands (text, lines, rectangles) work fine. For server-side PDFs, PDFKit is usually a better fit.
Can PDFKit run in the browser?
Yes, via a browserified build (pdfkit/js/pdfkit.standalone.js), but the bundle is large (around 1.5 MB gzipped) because it ships its own font subsetter and image decoder. For client-side rendering, jsPDF is smaller and more common.
Which library supports form filling?
pdf-lib supports reading and filling AcroForm fields (text fields, checkboxes, radio buttons, dropdowns) in existing PDFs. jsPDF has limited form creation support. PDFKit has basic form annotation support in recent versions.
Which library is the fastest?
For manipulation tasks (merge, split, extract pages), pdf-lib is the fastest because it streams the underlying PDF object graph without re-rendering. For primitive drawing, PDFKit has the smallest per-page overhead. jsPDF is the slowest but runs in environments the other two cannot reach.
Are these libraries actively maintained?
pdf-lib is maintained on a slow cadence (infrequent releases, issues triaged). jsPDF has an active maintainer and regular releases. PDFKit is maintained by a core team with frequent minor releases. All three are production-ready in 2026.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



