gpdf vs wkhtmltopdf vs Chromium —— 2026 年 PDF 生成方案对比
wkhtmltopdf 已归档。Chromium 每次请求消耗 170 MB。gpdf 用 13 µs 渲染一页,无需浏览器。2026 年的诚实对比。
TL;DR
wkhtmltopdf 已于 2023 年 1 月归档。无头 Chromium (Puppeteer / Playwright / chromedp / go-rod) 仍然可用,但要打包约 170 MB 的浏览器二进制,每个并发请求占用 50–120 MB 内存,冷启动 300–800 ms。gpdf 渲染一页 PDF 仅需 13 µs,零依赖,无需无头浏览器 —— 代价是不渲染任意 HTML+CSS。
本文的决策基准: 如果你的需求是"设计师交付一个 Tailwind 页面,要 pixel-perfect 出图",Chromium 仍是正确选择。如果需求是"发票、对账单、报表、证书、标签",原生方案属于完全不同的成本量级。
立场声明: 我们是 gpdf 的开发方。基准测试代码公开,trade-off 章节明确列出我们放弃了什么,用例矩阵也并未假装 gpdf 处处胜出。
三种架构并列
| 方案 | 代表工具 | 渲染引擎 | 二进制体积 | 单请求 RSS | 冷启动 | 许可证 |
|---|---|---|---|---|---|---|
| wkhtmltopdf | wkhtmltopdf CLI | QtWebKit fork (~2014) | ~40 MB | ~30–80 MB | ~150 ms | LGPLv3 |
| Chromium 系 | Puppeteer / Playwright / chromedp / go-rod | Blink + V8 (真正的 Chromium) | ~170 MB | ~50–120 MB | ~300–800 ms | BSD + 再分发限制 |
| 原生 (gpdf) | gpdf / signintech/gopdf / gofpdf† | 纯 Go PDF Writer | 0 依赖 | ~2–10 MB | 0 ms | MIT |
† gofpdf 和 go-pdf/fpdf 均已归档。Go 生态全景见我们的 2026 年 Go PDF 库横评。
在解释之前,先指出这张表上的三件事。
第一,wkhtmltopdf 的"二进制体积小"具有误导性。字节数小是因为它的 WebKit fork 十多年前就停止跟踪上游了。CVE 待办列表并不小。
第二,Chromium 不是 PDF 库 —— 它是一个恰好能打印的浏览器。那一列的所有成本都是浏览器成本。
第三,"0 ms vs 300 ms 冷启动"的差距,对每小时生成一次 PDF 的常驻服务器无关紧要。但对 Serverless (Lambda / Cloud Run / Workers) 和"以最快速度生成 1,000 份 PDF"的批处理来说,这是生死攸关的差距。
2026 年的 wkhtmltopdf
这一节你可能不必读。如果你的团队已经离开 wkhtmltopdf,直接跳到下一节。
对其他人: wkhtmltopdf 的开发实际上在 2022 年停止,项目仓库于 2023 年 1 月归档,维护者的告别说明明确推荐用 Chromium 作为替代品。原因是基础设施层面的。wkhtmltopdf 的渲染器是 QtWebKit —— WebKit 的一个分支,大约从 2014 年起就没有跟踪上游了。Qt 本身在 2016 年就废弃了 QtWebKit,改用基于 Chromium 的 QtWebEngine。wkhtmltopdf 至今仍在使用的 fork,是一个 12 年前的浏览器引擎。
具体而言,现代 CSS —— 完整 flex 规范、grid、大量 CSS 自定义属性、aspect-ratio、:has()、container queries、flex 的 gap、现代 color 函数 —— 要么渲染错误,要么根本不渲染。@font-face web 字体大多能用,但带可变轴的 web 字体不行。SVG 支持是部分的。WOFF2 支持来得很晚而且有 bug。
因此 2026 年"使用 wkhtmltopdf"有两层含义,两者都不妙。
你在用一个包含未打补丁 WebKit 代码的上游版本。 安全团队迟早会指出这一点,"项目已归档"不是修复方案。最后一次发布是 2020 年。从那以后的 CVE 工作由 Linux 发行版各自回移补丁,而非上游。
你维护着一个私有 fork。 需要有人读 Qt 和 WebKit 源码,回移补丁,并为你部署的每个平台重新构建。我们见过这种做法。代价是一名"宁愿做别的事"的工程师全职投入。
迁移问题是:用 Chromium (高保真,高成本) 还是用原生 PDF 生成器 (低成本,无 HTML/CSS) 替代。这是本文剩余部分的话题。
Chromium 系 PDF 生成的真实成本
无头 Chromium 在"确实需要浏览器"时是正确工具。成本出现在四个地方。
二进制。 Chromium 本身 ~170 MB。Playwright 捆绑一个已知良好的构建,Puppeteer 在安装时下载 (三种浏览器共 ~280 MB)。在容器镜像中,这会是你最大的一层,数量级上超过其他。在 Lambda zip 的 250 MB 上限内,光这一项就用满了。
单进程内存。 新启动的 Chromium 进程 RSS 约 50 MB。加载一个稍微复杂的页面 (真实 CSS、web 字体、几张图) 会推到 80–120 MB。数字随页面变化,下限不变。
冷启动。 启动 Chromium 并导航到 about:blank 在热机器上约 300 ms。加上 await page.goto(url) + 真实页面加载 + 字体获取 + await page.pdf(),首次请求更典型的是 500 ms 到 2 秒。保持 Chromium 进程池热是有帮助的,但在 Serverless 上无效 —— 每次扩容都要付冷启动代价。
运维表面。 浏览器是一片你本不打算涉足的决策大陆: CSP 如何处理、等 networkidle 还是 load 还是 domcontentloaded、是否禁用 JS、在 Docker 上如何设 --disable-dev-shm-usage、浏览器进程泄漏时怎么办。每个都不难。但全部加起来都是你不情愿去做的调试工作。
诚实的反方意见: 当你需要保真度时,你真的需要。设计师交给你一个 Figma 导出加 Tailwind 页面,要求自定义字体、渐变、SVG 图标精确呈现 —— 这是 Chromium 的活儿。用声明式文档 API 去硬刚,会烧掉一周,然后被设计师在第一次评审里打回来。
所以问题不是"用不用 Chromium",而是"我渲染的对象真的是网页吗"。
gpdf: 无需浏览器的原生渲染
gpdf 属于第三种 —— 纯 Go 的 PDF Writer。无 HTML,无 CSS,无无头浏览器。你用 Go (或 JSON,或 Go 模板) 描述文档,库直接输出 PDF 字节。
package main
import (
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
p.Row(document.Mm(12), func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("发票", template.FontSize(24), template.Bold())
})
})
p.Row(document.Mm(8), func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("Acme 公司", template.FontSize(11))
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("INV-2026-0517", template.FontSize(11), template.AlignRight())
})
})
// 后续: 明细行 + 合计
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
}
完整技术栈如上。容器中无 Chromium 二进制,无 npm install puppeteer,无 page.goto。Write 调用直接把 PDF 写入 writer —— 单页发票 CPU 时间 ~13 µs。
代价: 渲染器不知道 display: flex 是什么意思。它知道行、列 (12 列网格)、文本片段、图片、表格、条形码。对绝大多数规模化生成的文档 —— 发票、对账单、收据、报表、证书、标签、装箱单 —— 这套词汇够用。其余 (营销 PDF、设计师主导的宣传册、原本是网页的东西) 则不够用。
性能对比
把三种类型放在一起做基准测试在方法论上很棘手,因为它们解决的问题略有不同。我们还是要做。公平的对比是"相同的最终产物,三种实现": 一页发票,带表头、4×10 明细表、合计。
| 工作负载 | gpdf | wkhtmltopdf (CLI) | Chromium (Playwright page.pdf()) |
|---|---|---|---|
| 单页发票 | 13 µs | ~140 ms | ~280 ms (热) / ~1.2 s (冷) |
| 100 页分页报表 | 683 µs | ~3.4 s | ~6.1 s (热) |
| 单请求峰值 RSS | ~5 MB | ~70 MB | ~120 MB |
| 对容器镜像体积的影响 | 0 | +40 MB | +170 MB |
Apple M1, Go 1.25 (gpdf 侧)、wkhtmltopdf 0.12.6 二进制、Playwright 1.42 + 捆绑 Chromium。gpdf 的基准代码在 _benchmark/ —— 克隆下来在你的硬件上复现。
两个数字值得多看几眼。
单页发票的差距约 22,000 倍。大部分不是渲染本身,而是每次请求启动并销毁一个浏览器进程的成本。如果保持 Playwright 池温热,可以缩到 ~4 倍,但仍然是四个数量级的差距。
100 页报表的差距约 9,000 倍。这里渲染成本占主导,"启动浏览器"的常数开销被摊销。即便摊销之后,Chromium 仍然要为每个元素付布局成本,原生 PDF Writer 直接跳过。
在生产中真正咬人的是峰值 RSS 那一行。一个 Chromium 进程在 6 秒任务里占 ~120 MB,意味着 4 GB 容器大约能并发 30 份报表。同样容器跑 gpdf 能并发数千。
各方案的最佳场景
这不是"gpdf 包揽一切"的矩阵,也不该是。真实架构决策长这样。
| 用例 | 正确工具 | 原因 |
|---|---|---|
| 基于 Figma + Tailwind 的营销 PDF | Chromium (Playwright) | 对设计师意图的保真度比成本更重要。 |
| 月末 50,000 份月度对账单 | gpdf | 单份成本 × 数量 = 真金白银。不需要 CSS。 |
| 一次性"设计师要做一份宣传册" | Chromium (or InDesign) | 量小、CSS 重。一次性的事用对工具。 |
| SaaS 计费系统的发票 | gpdf | 量随营收增长。冷启动重要。布局是结构化的。 |
| 税务表格 / 合规申报 (PDF/A) | gpdf (or unidoc) | PDF/A 合规、签名、审计跟踪。浏览器不处理这些。 |
| 带图表截图的 BI 仪表盘报表 | Chromium | 图表是重点,PDF 只是导出方式。 |
| "把 Markdown 印出来" / 文档 PDF | gpdf 或 Chromium | 都可以。用成本换保真度。 |
| 遗留 wkhtmltopdf 迁移 | HTML 简单则 gpdf,CSS 重则 Chromium | 先审计模板。 |
模式: 数量 × 单请求成本 vs 设计保真度。前者占主导,原生胜出。后者占主导,Chromium 胜出。wkhtmltopdf 在 2026 年的这张矩阵里没有任何位置。
我们不假装不存在的取舍
我们一直在暗示这件事,值得单独成节说清楚。
gpdf 不渲染 HTML 或 CSS。如果你现有系统是"我们有一个 HTML 邮件模板,顺便也用它打印 PDF",迁移到 gpdf 就意味着用 gpdf 的 builder API 重写那个模板。一个模板,一下午就够。30 个由设计师维护的营销模板库,那是一个项目。
我们也不渲染 @font-face 的 web 字体。你在文档构建时把 TTF/OTF 文件传给 gpdf。CJK 字体是 first-class —— 我们写过 为什么不用 CGO 也能渲染 CJK —— 但字体文件由开发者负责交付。
我们不妥协的地方: 速度、内存、可部署性、依赖足迹。取舍是用功能表面付的,不是用生产成本付的。我们的判断: 很多生成高量级结构化文档的团队一直在为他们其实不需要的浏览器付费。对这些团队,原生路径是正确答案。但并非每个团队都该用 gpdf。
代码: 同一份发票的三种实现
如果想直观感受 API 差异,把三种实现并排放。
Chromium (Playwright, Node):
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const html = fs.readFileSync('invoice.html', 'utf8');
await page.setContent(html, { waitUntil: 'networkidle' });
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '20mm', right: '20mm' },
});
await browser.close();
})();
外加一份你自己维护的 invoice.html、捆绑的 Chromium 二进制 (~170 MB)、字体加载方案 (web 字体? base64 内嵌? --font-render-hinting?)。在 Tailwind 模板上能漂亮地工作。维护对象是 HTML。
wkhtmltopdf (shell):
wkhtmltopdf --enable-local-file-access \
--margin-top 20mm --margin-bottom 20mm --margin-left 20mm --margin-right 20mm \
invoice.html invoice.pdf
外加 wkhtmltopdf 二进制、一份避免使用 QtWebKit-2014 不懂的 CSS 的 HTML 模板 (实务上: 不能用 grid、flex 要小心、不能用 :has()、CSS 自定义属性部分可用)。外加二进制被审计标记时的安全沟通。
gpdf (Go):
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
invoiceHeader(p, "INV-2026-0517", "Acme 公司")
invoiceTable(p, lineItems)
invoiceTotals(p, subtotal, tax, total)
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
外加你针对 builder API 自己写的三个 Go 函数。没有模板文件,没有二进制依赖,没有独立的渲染步骤。可作为单个 Go 二进制部署到 FROM scratch 容器。
正确的阅读方式不是"哪个最短",而是"我愿意维护哪种表面积"。Chromium 的表面积是 HTML + CSS + 浏览器。wkhtmltopdf 的表面积是 HTML + CSS + 一个十年前的浏览器。gpdf 的表面积是 Go。
FAQ
2026 年 wkhtmltopdf 真的不能用了吗?
"不能用"说得太重。"不建议"更准确。它还能跑,简单模板还能产出正确 PDF。不建议在新项目里采用的原因: 项目已归档、WebKit fork 是 2014 年的代码、安全审计必然标红、官方替代建议是"用 Chromium"。如果生产里已经在用,你还有时间迁移;但没有时间继续往上加新依赖。
直接接受 Chromium 的成本不行吗?
对大多数工作负载来说,可以。上面的决策矩阵把营销 PDF 和设计师主导的文档明确放在 Chromium 列。这篇文章存在的原因是,Chromium 也被用在发票、对账单、报表上 —— 这些工作负载并不需要浏览器的保真度。这种情况下成本会出现在 AWS 账单上。
不用 Chromium 的 HTML-to-PDF (例如 html2pdf 或 jsPDF) 呢?
那是浏览器端的 JS 库,把 HTML 渲染到 canvas 再转 PDF。保真度比 Chromium 差很多 (大部分现代 CSS 不工作),性能也比原生差 (渲染两次: HTML → canvas → PDF)。它们有自己的小众场景 —— 在浏览器里做无服务端的客户端 PDF 生成 —— 但不在本文对比之列。
gpdf 支持 PDF/A 或数字签名吗?
支持。gpdf.WithPDFA(...) 提供 PDF/A-1b 和 PDF/A-2b 合规,gpdf.SignDocument(...) 提供 PKCS#7 签名 (含 RFC 3161 时间戳)。两者都在 MIT 核心库里 —— 没有附加包,没有商业许可。
gpdf 跟其他 Go PDF 库 (非浏览器系) 比怎么样?
这是另一个问题。短回答: gofpdf 和 go-pdf/fpdf 已归档;signintech/gopdf 在维护但偏底层 (没有布局网格);Maroto v2 在维护但底层是已归档的 gofpdf;unidoc 是商业产品。完整对比见 2026 年 Go PDF 库横评。
上手 gpdf
gpdf 是一个 Go 的 PDF 库。MIT,零依赖,原生 CJK。
go get github.com/gpdf-dev/gpdf
延伸阅读
- 为什么 gpdf 比其他 Go PDF 库快 10–30 倍 —— 本文数字背后的架构
- 2026 年 Go PDF 库横评 —— Go 原生库之间的对比
- 从 gofpdf 迁移到 gpdf —— 如果正打算离开归档库