puppeteer vs html-pdf vs pdfkit
Node.js 环境下的 PDF 生成方案选型
puppeteerhtml-pdfpdfkit类似的npm包:

Node.js 环境下的 PDF 生成方案选型

html-pdf、pdfkit 和 puppeteer 代表了三种不同的 PDF 生成路径。html-pdf 是早期的 HTML 转 PDF 方案,但已停止维护;pdfkit 是一个底层的 PDF 绘制库,适合通过代码生成结构化文档;puppeteer 则通过控制无头浏览器来渲染页面,是目前 HTML 转 PDF 的主流标准。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
puppeteer11,547,24295,69943.2 kB2591 天前Apache-2.0
html-pdf03,574-4685 年前MIT
pdfkit010,72110.5 MB3342 个月前MIT

Node.js PDF 生成方案:html-pdf vs pdfkit vs puppeteer

在 Node.js 后端生成 PDF 文件是常见需求,无论是发票、报表还是导出功能。html-pdf、pdfkit 和 puppeteer 是三个经常被提及的库,但它们的底层原理和适用场景完全不同。本文将深入对比它们的技术细节,帮助你做出正确的架构决策。

⚠️ 维护状态与安全警告

在选择库之前,必须了解其维护状态。这直接影响项目的长期安全性。

html-pdf 已处于废弃状态。

  • 它依赖 PhantomJS,而 PhantomJS 早在 2018 年就已停止维护。
  • 存在未修复的安全漏洞,且不支持现代 CSS 标准。
  • 结论:不要在新项目中使用。
// html-pdf: 已废弃的 API 示例
const pdf = require('html-pdf');
// 不推荐:依赖旧的 PhantomJS 二进制文件
pdf.create('<h1>Hello</h1>').toFile('./file.pdf', (err, res) => {
  if (err) return console.log(err);
  console.log(res.filename);
});

pdfkit 维护良好,专注于底层绘制。

  • 纯 JavaScript 实现,无外部二进制依赖。
  • 适合长期维护的文档生成服务。
// pdfkit: 稳定的 API 示例
const PDFDocument = require('pdfkit');
const doc = new PDFDocument();
// 推荐:直接绘制矢量内容
doc.text('Hello', 100, 100);
doc.save('./file.pdf');

puppeteer 由 Chrome 团队维护,更新频繁。

  • 跟随 Chrome 版本更新,支持最新 Web 标准。
  • 社区活跃,文档完善。
// puppeteer: 现代标准的 API 示例
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // 推荐:使用现代无头浏览器渲染
  await page.setContent('<h1>Hello</h1>');
  await page.pdf({ path: './file.pdf' });
  await browser.close();
})();

🛠️ 核心工作原理:渲染引擎 vs 矢量绘制

这三个库生成 PDF 的方式截然不同,决定了它们能处理什么样的内容。

html-pdf 使用旧的渲染引擎。

  • 内部调用 PhantomJS 将 HTML 渲染为图像或 PDF。
  • 无法处理 Flexbox 或 Grid 等现代布局。
  • 适合简单的旧式 HTML 模板。
// html-pdf: 渲染简单 HTML
pdf.create('<div style="float:left">Content</div>').toFile('./out.pdf');

pdfkit 直接操作 PDF 指令。

  • 没有 HTML 概念,只有坐标、路径和文本。
  • 你需要手动计算每个元素的位置。
  • 适合生成固定格式的票据或证书。
// pdfkit: 手动绘制坐标
doc.rect(100, 100, 200, 50); // 画矩形
doc.text('Invoice', 110, 115); // 写文字

puppeteer 使用完整的 Chrome 引擎。

  • 像真实用户一样加载页面,执行 JS 和 CSS。
  • 支持打印媒体查询 @media print。
  • 适合复杂的企业报表或 dashboard 导出。
// puppeteer: 渲染完整页面
await page.goto('https://example.com/report', { waitUntil: 'networkidle0' });
await page.pdf({ format: 'A4', printBackground: true });

🎨 CSS 与布局支持能力

前端开发者最关心的是 CSS 支持程度。这决定了你能否复用现有的前端代码。

html-pdf 支持有限的 CSS。

  • 支持基本的盒模型和字体。
  • 不支持 CSS Grid、Flexbox 或现代动画。
  • 样式容易在不同环境中不一致。
/* html-pdf: 仅支持旧式 CSS */
.box { float: left; width: 50%; } /* 可行 */
.box { display: flex; } /* 可能失效 */

pdfkit 不支持 CSS。

  • 样式需要通过 API 方法设置(如 fontSize、fillColor)。
  • 布局需要手动计算坐标或使用插件。
  • 完全脱离 Web 技术栈。
// pdfkit: 通过 API 设置样式
doc.fontSize(20).fillColor('blue').text('Title');

puppeteer 支持完整的 CSS。

  • 支持 Flexbox、Grid、变量、伪类等所有现代特性。
  • 可以复用项目中的 CSS 文件。
  • 支持 print 媒体查询来隐藏按钮或导航栏。
/* puppeteer: 支持完整 CSS */
.container { display: grid; grid-template-columns: 1fr 1fr; }
@media print { .no-print { display: none; } }

💾 资源占用与性能表现

服务器资源是有限的,不同的库对内存和 CPU 的要求不同。

html-pdf 占用中等,但启动慢。

  • 需要启动 PhantomJS 进程。
  • 并发高时容易崩溃。
  • 已不再优化,性能瓶颈明显。
// html-pdf: 高并发下不稳定
// 无内置并发控制,需自行管理队列

pdfkit 占用最低,速度最快。

  • 纯 JS 计算,无外部进程。
  • 适合高并发场景。
  • 生成速度快,内存占用小。
// pdfkit: 适合高并发
// 直接在当前进程运行,无额外开销

puppeteer 占用最高,启动较重。

  • 每个实例需要启动一个 Chrome 进程。
  • 内存消耗大,需要限制并发数量。
  • 建议使用无头模式 --no-sandbox 优化容器环境。
// puppeteer: 需控制并发
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
// 建议配合 p-limit 控制同时打开的页面数

📊 总结对比表

特性html-pdfpdfkitpuppeteer
维护状态❌ 已废弃✅ 活跃✅ 活跃
渲染引擎PhantomJS (旧)无 (矢量绘制)Chrome/Chromium
HTML 支持基础 HTML不支持完整支持
CSS 支持有限 (旧标准)不支持完整 (现代标准)
资源占用中低高
适用场景旧项目维护发票、证书、图表报表、网页导出

💡 最终建议

html-pdf 是历史遗留产物 — 除非你被锁定在旧系统中,否则请立即停止使用。它的安全风险和兼容性问题是不可接受的。

pdfkit 是数据驱动文档的最佳选择 — 如果你需要生成发票、收据或带有动态图表的证书,且不需要 HTML 布局,它的性能和控制力是最好的。

puppeteer 是 Web 技术栈的自然延伸 — 如果你的团队熟悉 HTML/CSS,且需要生成复杂的报表或导出用户看到的页面,它是唯一正确的选择。虽然资源占用较高,但可以通过集群管理来优化。

核心原则:不要为了生成 PDF 而重新学习一套布局系统。如果能用 HTML/CSS 解决,就用 puppeteer;如果需要精细的矢量控制,就用 pdfkit。

如何选择: puppeteer vs html-pdf vs pdfkit

  • puppeteer:

    当你需要将现有的 HTML/CSS 页面转换为 PDF 时选择它。它能完美支持现代 CSS 布局、字体和打印媒体查询,是生成报表的最佳方案。

  • html-pdf:

    不建议在任何新项目中使用。该库依赖已废弃的 PhantomJS 引擎,存在安全漏洞且不再维护。如果维护旧代码,应尽快迁移至 puppeteer。

  • pdfkit:

    当你需要从零开始绘制 PDF 元素(如动态发票、图表、证书)时选择它。它不依赖浏览器引擎,适合生成由数据驱动的矢量图形文档。

puppeteer的README

Puppeteer

build npm puppeteer package

Puppeteer is a JavaScript library which provides a high-level API to control Chrome or Firefox over the DevTools Protocol or WebDriver BiDi. Puppeteer runs in the headless (no visible UI) by default

Get started | API | FAQ | Contributing | Troubleshooting

Installation

npm i puppeteer # Downloads compatible Chrome during installation.
npm i puppeteer-core # Alternatively, install as a library, without downloading Chrome.

:::note

Modern package managers (including npm (see the RFC), pnpm, Yarn, Bun, and Deno) block dependency install scripts by default. If the install script is blocked, Puppeteer will not download the browser during installation, leading to runtime errors.

You can manually download the required browsers after installation by running:

npx puppeteer browsers install

Alternatively, you can configure your package manager to allow the install script to run (for example, with npm, by adding "puppeteer" to "allowScripts" in your package.json).

:::

MCP

Install chrome-devtools-mcp, a Puppeteer-based MCP server for browser automation and debugging.

Puppeteer also supports the experimental WebMCP API.

Example

import puppeteer from 'puppeteer';
// Or import puppeteer from 'puppeteer-core';

// Launch the browser and open a new blank page.
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Navigate the page to a URL.
await page.goto('https://developer.chrome.com/');

// Set the screen size.
await page.setViewport({width: 1080, height: 1024});

// Open the search menu using the keyboard.
await page.keyboard.press('/');

// Type into search box using accessible input name.
await page.locator('::-p-aria(Search)').fill('automate beyond recorder');

// Wait and click on first result.
await page.locator('.devsite-result-item-link').click();

// Locate the full title with a unique string.
const textSelector = await page
  .locator('::-p-text(Customize and automate)')
  .waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);

// Print the full title.
console.log('The title of this blog post is "%s".', fullTitle);

await browser.close();