性能基准测试
真实的性能基准测试,将 TCPDF-Next 与三个成熟的 PHP PDF 库进行对比:TCPDF、DomPDF 和 mPDF。所有测试均在相同的硬件上、受控的 Docker 环境中执行。结果取 20 次迭代的中位数,以消除离群值噪声。
测试环境
| 参数 | 数值 |
|---|---|
| CPU | Intel Core i9-13900K (x86-64) |
| 内存 | 64 GB DDR5(Docker 限制为 16 GB) |
| Docker | 4 CPUs, 16 GB RAM, Debian bookworm |
| PHP | 8.5.3(CLI,OPcache 启用,JIT 启用) |
| TCPDF-Next | 1.7.0 |
| TCPDF | 6.10.1 |
| DomPDF | v3.1.4 |
| mPDF | v8.2.7 |
| Artisan (Chrome) | 通过 CDP 调用 Headless Chromium |
| RoadRunner | spiral/roadrunner-http ^3.6(HTTP 吞吐量测试) |
| 预热 | 3 次迭代(丢弃) |
| 测量 | 20 次迭代(报告中位数) |
| 计时方式 | hrtime(true) 纳秒精度挂钟时间 |
交互式对比
PHP 8.5.3 + OPcache + JIT · Docker 4 CPUs / 16 GB · i9-13900K · Median of 20 runs
生成速度
每个场景在 3 次预热迭代后运行 20 次,报告中位数生成时间。
简单文档(1 页)
一页 A4 文档,包含标题和基本格式化文字,使用内置 Helvetica 字体。无图片、无表格。
| 库 | 时间(ms) |
|---|---|
| TCPDF-Next | 0.68 |
| TCPDF | 2.55 |
| DomPDF | 4.16 |
| mPDF | 6.71 |
TCPDF-Next 在最简单的场景中以不到 1 ms 完成——比 TCPDF 快 3.8x,比 DomPDF 快 6.1x,比 mPDF 快 9.9x。
发票(2 页)
两页发票文档,包含 25 行表格行项目、小计、页眉、页脚和一张 Logo 图片。
| 库 | 时间(ms) |
|---|---|
| TCPDF | 1.96 |
| TCPDF-Next | 2.01 |
| mPDF | 15.86 |
| DomPDF | 17.33 |
TCPDF-Next 与 TCPDF 在发票场景中几乎持平(~1.0x)。两者均显著优于 mPDF(慢 7.9x)和 DomPDF(慢 8.6x)。
100 页报告
100 页文档,包含密集的混合内容:标题、段落和结构化数据。
| 库 | 时间(ms) |
|---|---|
| TCPDF-Next | 34.29 |
| TCPDF | 105.39 |
| mPDF | 1,106.59* |
| DomPDF | 2,129.12 |
TCPDF-Next 在 34.29 ms 内完成 100 页报告——比 TCPDF 快 3.1x,比 mPDF 快 32.3x,比 DomPDF 快 62.1x。
JIT 兼容性说明
*mPDF 的 100 页报告结果是在 JIT 禁用(opcache.jit=0)的情况下测量的,原因是 mPDF 代码路径中出现 PHP JIT 段错误(SIGSEGV,退出代码 139)。OPcache 字节码缓存仍保持启用。这是一类已知的 PHP JIT 缺陷,影响某些复杂的循环模式。mPDF 的所有其他场景均在 JIT 启用下运行。
TrueType 文档(1 页)
一页 A4 文档,使用 DejaVu Sans(约 700 KB 的 TrueType 字体)。此场景暴露了真实的字体文件解析成本——不同于 Helvetica(内置 Base14 字体,无需任何文件 I/O)。
| 库 | 时间(ms) |
|---|---|
| TCPDF-Next | 4.08 |
| TCPDF | 12.11 |
| mPDF | 16.51 |
| DomPDF | 24.14 |
TCPDF-Next 解析和嵌入 TrueType 字体的速度比 TCPDF 快 3.0x,比 mPDF 快 4.0x,比 DomPDF 快 5.9x。
相对速度(所有场景)
所有数值相对于 TCPDF-Next(1.0x 基线)。数值越低越快。
| 场景 | TCPDF-Next | TCPDF | DomPDF | mPDF |
|---|---|---|---|---|
| 简单文档 | 1.0x | 3.8x | 6.1x | 9.9x |
| 发票 | 1.0x | ~1.0x | 8.6x | 7.9x |
| 100 页报告 | 1.0x | 3.1x | 62.1x | 32.3x |
| TrueType 文档 | 1.0x | 3.0x | 5.9x | 4.0x |
HTML 转 PDF
HTML 处理方式
不同的库采用根本不同的方式将 HTML 转换为 PDF。理解这些差异对于解读基准测试结果至关重要:
直译(TCPDF-Next、TCPDF) —— 内置 HTML 解析器对 HTML 标签进行词法分析,并在单次流式处理中将其直接映射为 PDF 绘制命令(Cell、MultiCell、Image)。这种方式速度极快,但仅支持基本 HTML 标签和内联 CSS——不支持 Flexbox、Grid 和复杂 CSS 选择器。
CSS 排版引擎(DomPDF、mPDF) —— 这些库以 HTML 作为其主要接口进行设计。DomPDF 构建完整的 DOM 树,应用 CSS 级联规则(优先级、继承),并在渲染为 PDF 之前计算盒模型布局。mPDF 的 WriteHTML() 同样通过其自有的 CSS 排版引擎处理 HTML。两者比直译解析器支持更多的 CSS 特性(浮动、定位元素、样式化表格),但仍达不到完整的浏览器级 CSS3 水平。
完整浏览器渲染(Artisan / Chrome) —— TCPDF-NextArtisan 将渲染委托给通过 Chrome DevTools Protocol (CDP) 调用的 Headless Chromium。这提供了像素级精确的 CSS3 支持:Flexbox、Grid、Web 字体、媒体查询、CSS 变量——输出与 Chrome 浏览器渲染的结果完全一致。
基准测试对比的是每个库的原生方式:TCPDF-Next 和 TCPDF 使用其内置的直译解析器;DomPDF 和 mPDF 使用其 CSS 渲染引擎(即其主要 API);Artisan 使用 Chrome。
结果
| 库 | 处理方式 | 时间(ms) |
|---|---|---|
| TCPDF-Next | 直译 | 1.51 |
| TCPDF | 直译 | 6.60 |
| DomPDF | CSS 排版引擎 | 13.69 |
| mPDF | CSS 排版引擎 | 29.63 |
| Artisan (Chrome) | 完整浏览器渲染 | 66.70 |
相对时间(HTML 转 PDF)
| 库 | 相对倍数 |
|---|---|
| TCPDF-Next | 1.0x |
| TCPDF | 4.4x |
| DomPDF | 9.0x |
| mPDF | 19.6x |
| Artisan (Chrome) | 44.1x |
TCPDF-Next 的直译解析器实现了低于 2 ms 的性能——比 TCPDF 基于正则表达式的解析器快 4.4x,比 DomPDF 的 CSS 排版引擎快 9.0x,比 mPDF 快 19.6x。Artisan (Chrome) 慢 44.1x,但提供了其他任何库都无法匹敌的完整 CSS3 保真度。
Artisan Chrome —— 分阶段细分
将 Artisan (Chrome) 管线分解为两个阶段:
- Chrome CDP 渲染 —— Headless Chrome 通过
printToPDF将 HTML 转换为 PDF 字节 - PDF 导入 + 嵌入 —— TCPDF-Next 解析 Chrome 的 PDF,将页面提取为 Form XObject,并嵌入到目标文档中
| 阶段 | 中位数(ms) | 均值(ms) | 最小值(ms) | 最大值(ms) | 标准差 |
|---|---|---|---|---|---|
| Chrome CDP 渲染 | 81.17 | 81.17 | 65.51 | 95.80 | 4.84 |
| PDF 导入 + 嵌入 | 1.96 | 2.08 | 1.60 | 2.87 | 0.40 |
| 合计 | 83.35 | 83.29 | 68.20 | 97.56 | 4.70 |
时间分布: Chrome CDP = 97.4% | PDF 导入 = 2.3%
Chrome 的 printToPDF 占据了管线 97.4% 的时间。PDF 导入阶段(PdfReader + PageImporter + XObject 嵌入)仅增加约 2 ms——可忽略不计的开销。
标准测试与分阶段测试的说明
标准 Artisan 测试(66.70 ms)使用集成的 writeHtmlChrome() 方法并启用 BrowserPool 长连接。分阶段测试(总计 83.35 ms)对每个阶段单独计时,增加了测量开销。两者均使用相同的 Chrome 长连接实例——初始 Chromium 启动的约 250 ms 冷启动成本被排除在外,因为它是一次性成本,可在数千次请求中分摊。
何时使用哪种方式
对于简单 HTML(表格、基本格式化),使用 TCPDF-Next 的内置 HTML 解析器(1.51 ms)。对于需要像素级精确保真度的复杂 CSS3 布局(Flexbox、Grid、Web 字体),使用 Artisan——约 67 ms 的开销换来 Chrome 渲染引擎的完整能力。
Worker 生命周期(DocumentFactory 与 Standalone)
TCPDF-Next 提供了 DocumentFactory 模式,专为长时间运行的 PHP Worker(RoadRunner、Swoole、Laravel Octane)设计。工厂在启动时预初始化并锁定共享注册表(FontRegistry、ImageRegistry)。每个 HTTP 请求从工厂创建一个轻量级的一次性 Document——消除了逐请求的初始化开销。
本节将 DocumentFactory(共享、锁定的注册表)与 createStandalone()(每次调用创建全新的注册表)进行对比。
内置字体(Helvetica)
| 模式 | 中位数(ms) | 峰值内存(MB) | 文件大小(KB) |
|---|---|---|---|
| DocumentFactory | 0.60 | 4.0 | 3.3 |
| createStandalone() | 0.70 | 4.0 | 3.3 |
结果:基本相当(比率 0.86x)。使用内置字体(Helvetica)时,两种模式性能相同,因为没有需要缓存的字体文件解析。DocumentFactory 的真正优势体现在 TrueType 字体上。
TrueType 字体(DejaVu Sans)
这是 DocumentFactory 价值主张的关键测试。与上面的 Helvetica 测试(内置字体,零解析)不同,此测试使用 DejaVu Sans(约 700 KB 的 TrueType 字体)。DocumentFactory 在启动时预注册并缓存已解析的字体数据——后续请求跳过所有字体文件 I/O。createStandalone() 则必须在每一次请求中解析 .ttf 文件。
| 模式 | 中位数(ms) | 峰值内存(MB) | 文件大小(KB) |
|---|---|---|---|
| Factory(TTF 缓存) | 2.60 | 6.0 | 24.5 |
| Standalone(TTF 解析) | 4.09 | 6.0 | 24.3 |
Factory 加速比:1.6x —— 缓存字体解析消除了每次请求约 1.5 ms 的开销。在处理 1,000 请求/分钟的 RoadRunner/Swoole Worker 中,这意味着每分钟节省约 25 秒的 CPU 时间。
峰值内存使用
所有数值单位为 MB(中位数)。每个库的基准测试在独立的 PHP 子进程中运行——仅加载所需的自动加载器,因此 memory_get_peak_usage() 反映的是该库单独的实际内存成本。
标准场景
| 场景 | TCPDF-Next | TCPDF | DomPDF | mPDF |
|---|---|---|---|---|
| 简单文档 | 4.0 | 12.0 | 6.0 | 14.0 |
| 发票 | 4.0 | 12.0 | 12.0 | 14.0 |
| 100 页报告 | 4.0 | 12.0 | 66.0 | 27.9* |
| TrueType 文档 | 6.0 | 14.0 | 20.0 | 16.0 |
TCPDF-Next 从 1 页到 100 页文档始终保持 4 MB 的内存占用,通过紧凑的页面对象和共享资源引用展现了高效的内存管理。
HTML 转 PDF 内存
| 库 | 峰值内存(MB) |
|---|---|
| TCPDF-Next | 4.0 |
| Artisan (Chrome) | 4.0 |
| DomPDF | 10.0 |
| TCPDF | 12.0 |
| mPDF | 18.0 |
Artisan (Chrome) 仅测量 PHP 端内存——Headless Chrome 进程有其独立的内存空间,由操作系统管理。
输出文件大小
所有数值单位为 KB(中位数)。
标准场景
| 场景 | TCPDF-Next | TCPDF | DomPDF | mPDF |
|---|---|---|---|---|
| 简单文档 | 3.3 | 7.1 | 1.7 | 28.0 |
| 发票 | 5.0 | 9.2 | 4.0 | 30.2 |
| 100 页报告 | 96.4 | 100.8 | 128.7 | 181.1* |
| TrueType 文档 | 24.7 | 101.3 | 16.1 | 42.4 |
DomPDF 通过激进的内容流优化为简单文档生成最小的文件(1.7 KB)。TCPDF-Next 通过 PDF 2.0 交叉引用流和对象流生成紧凑的输出。TCPDF 嵌入了显著更大的 TrueType 字体子集(101.3 KB vs 24.7 KB)。
HTML 转 PDF 文件大小
| 库 | 文件大小(KB) |
|---|---|
| DomPDF | 5.3 |
| TCPDF-Next | 6.6 |
| TCPDF | 12.6 |
| Artisan (Chrome) | 36.9 |
| mPDF | 46.0 |
Artisan (Chrome) 的输出较大(36.9 KB),因为 Chrome 的 printToPDF 生成的是包含嵌入资源的完整独立 PDF。
吞吐量
吞吐量测试使用简单文档场景持续运行 30 秒。数值反映了负载下的持续生成能力。
标准吞吐量(文档/秒)
| 模式 | TCPDF-Next | TCPDF | DomPDF | mPDF |
|---|---|---|---|---|
| 单线程 | 2,605 | 1,169 | 233 | 130 |
| 4 Workers | 9,221 | 4,163 | 841 | 487 |
每分钟文档数
| 模式 | TCPDF-Next | TCPDF | DomPDF | mPDF |
|---|---|---|---|---|
| 单线程 | 156,284 | 70,134 | 13,956 | 7,800 |
| 4 Workers | 553,280 | 249,752 | 50,484 | 29,194 |
使用 4 个 Worker 时,TCPDF-Next 可维持每秒超过 9,200 份文档——每分钟超过 553,000 份文档。这是 TCPDF 吞吐量的 2.2 倍、DomPDF 的 11.0 倍、mPDF 的 19.0 倍。
Worker 生命周期吞吐量
对比使用内置 Helvetica 字体时 DocumentFactory 与 createStandalone() 的吞吐量。
| 模式 | DocumentFactory | createStandalone() |
|---|---|---|
| 单线程 | 2,490 | 2,515 |
| 4 Workers | 9,074 | 9,191 |
使用内置字体时,DocumentFactory 与 createStandalone() 产生等效的吞吐量——没有字体解析需要缓存。
TrueType 文档吞吐量
使用 TrueType 字体(DejaVu Sans)的吞吐量——暴露每个库真实的字体解析开销。
| 库 | 单线程(文档/秒) |
|---|---|
| TCPDF-Next | 242 |
| TCPDF | 81 |
| mPDF | 50 |
| DomPDF | 30 |
TCPDF-Next 的 TrueType 吞吐量是 TCPDF 的 3.0 倍、mPDF 的 4.8 倍、DomPDF 的 8.0 倍——反映了其高效的字体子集化和缓存机制。
Worker 生命周期 TTF 吞吐量
DocumentFactory(缓存 TrueType 字体数据)与 createStandalone()(每次请求都解析 TTF)的对比。
| 模式 | Factory(TTF 缓存) | Standalone(TTF 解析) |
|---|---|---|
| 单线程 | 364 | 243 |
| 4 Workers | 1,327 | 871 |
Factory 吞吐量优势:1.5x(单线程)。缓存的 TrueType 字体数据消除了逐请求的 .ttf 解析开销。使用 4 个 Worker 时,Factory 达到 1,327 文档/秒——比 Standalone 提升 52.4%。
RoadRunner HTTP 吞吐量
使用 RoadRunner 结合 DocumentFactory Worker 模式的真实 HTTP 服务器基准测试。通过 ab (Apache Bench) 测量。
| 配置 | 文档/秒 | 平均延迟(ms) | p50(ms) | p99(ms) | 失败数 |
|---|---|---|---|---|---|
| 1 worker / 1 并发 | 1,320 | 0.76 | 1 | 1 | 0 |
| 4 workers / 4 并发 | 4,812 | 0.83 | 1 | 1 | 0 |
HTTP 开销(与原始 pcntl_fork 对比):
- 单线程:49.3% 开销(1,320 vs 2,605 文档/秒)
- 多 Worker:47.8% 开销(4,812 vs 9,221 文档/秒)
约 48% 的开销来自完整 HTTP 协议栈的成本(TCP accept、HTTP 解析、响应写入)。即使有此开销,TCPDF-Next 在 RoadRunner 后端仍可达到每秒 4,812 个真实 HTTP PDF 响应,延迟亚毫秒级,零失败请求。
测试方法
- 环境: 所有库在同一 Docker 容器内运行(PHP 8.5.3,Debian bookworm),配置完全相同。
- 资源限制: 容器通过 Docker 资源约束限制为 4 CPUs 和 16 GB RAM。
- 运行时: 所有库均启用 OPcache 和 JIT。弃用警告全局抑制以确保公平计时。
- 子进程隔离: 每个库/场景组合在独立的 PHP 进程(
exec())中运行,以获取准确的内存测量——其他库的自动加载器类不会污染memory_get_peak_usage()。 - API 对等性: TCPDF-Next 和 TCPDF 在非 HTML 场景中使用原生
Cell/MultiCellAPI。DomPDF 和 mPDF 使用等效的 HTML 标记(其原生接口)。 - TrueType 字体测试 使用 DejaVu Sans(约 700 KB
.ttf)以暴露真实的字体解析成本;Helvetica(Base14)测试展示零开销基线。 - Artisan (Chrome) 使用通过 CDP 调用的 Headless Chromium 实现像素级精确的 CSS3 渲染(通过 CSP 禁用 JavaScript)。
- Artisan 分阶段测试 将 Chrome 渲染分解为:阶段 1(Chrome CDP
printToPDF)vs 阶段 2(PdfReader + PageImporter + 嵌入)。 - Worker 生命周期 对比
DocumentFactory(共享 FontRegistry + 锁定、ImageRegistry)与createStandalone()(每次调用创建全新注册表)。 - Worker 生命周期 TTF 展示
DocumentFactory的核心价值:跨数千次 Worker 请求缓存 TrueType 字体数据。 - RoadRunner HTTP 使用 roadrunner-server/roadrunner 结合
DocumentFactoryWorker 模式,通过ab(Apache Bench) 测量。 - 预热: 在测量开始前执行并丢弃 3 次预热迭代,确保 OPcache 和 JIT 已完全预热。
- 迭代: 每个场景 20 次测量迭代。报告中位数以消除离群值噪声。
- 吞吐量: 测试持续运行 30 秒。
- 计时:
hrtime(true)提供纳秒精度的挂钟时间测量。 - 内存:
memory_get_peak_usage(true)报告真实(RSS)峰值内存。 - 文件大小: 输出刷新到磁盘后通过
filesize()测量。
数据解读注意事项
- 子进程内存隔离: 每个库的延迟基准测试在其独立的 PHP 子进程中运行。仅加载所需的自动加载器,因此
memory_get_peak_usage()反映的是该库单独的实际内存成本——而非来自其他库的累积自动加载器污染。 - Artisan (Chrome) 使用 BrowserPool 长连接: Chrome 进程在迭代间保持活跃,匹配生产环境行为(RoadRunner/Swoole/Octane)。冷启动开销(初始 Chromium 启动约 250 ms)被排除——它是在数千次请求中分摊的一次性成本。
- 延迟与吞吐量的差异: 单次运行的延迟测量包含
gc_collect_cycles()、memory_reset_peak_usage()和hrtime()开销(约 0.3 ms)。吞吐量测试在紧密循环中运行,没有测量开销,因此其每文档时间(1000/每秒文档数)低于单次运行的中位数。吞吐量数字更准确地反映了生产环境性能。 - Helvetica 与 TrueType: Helvetica 是内置 PDF 字体(Base14),无需任何文件 I/O。TrueType 场景使用 DejaVu Sans,需要解析
.ttf文件(约 700 KB)。DocumentFactory的缓存 FontRegistry 优势仅在 TrueType 字体下体现。 - JIT 兼容性(*): 标注 * 的数值是在 JIT 禁用(
opcache.jit=0)的情况下测量的,原因是该库代码路径中出现 PHP JIT 段错误(SIGSEGV,退出代码 139)。OPcache 字节码缓存仍保持启用。这是一类已知的 PHP JIT 缺陷,影响某些复杂的循环模式。
重现基准测试
基准测试套件包含在仓库中。要重现这些结果:
cd benchmark
docker compose up --build结果将在运行结束时打印到标准输出。Docker 配置确保无论宿主操作系统如何,环境都是一致的。
延伸阅读
- 从 TCPDF 迁移 —— 分步迁移指南
- 性能调优 —— 流式模式、内存优化与缓存策略