← dompdf.js Studio

dompdf.js 增量渲染与分段生成指南

处理超长文档时,最忌讳的思路是一次把全部内容塞进去:巨型 DOM 一次性快照,内存瞬间吃紧,渲染长时间无响应,出了问题还难定位。dompdf.js 的 addPage 接口天然支持分段构建——每次调用添加一页或一个内容块,文档在多次调用中逐步成型,这为增量渲染提供了最直接的实现方式。配合分块快照、进度反馈与任务队列,可以把千页级文档的导出拆成可观测、可恢复、可并行的小任务,页面始终保持响应。本文先解释增量渲染的核心思路与适用场景,再给出 addPage 分段构建的完整代码示例,然后介绍进度回调、内存优化、批量导出队列与常见问题排查,帮助你把大文档导出做成稳定的工程能力。

什么是增量渲染:拆大为小

增量渲染的核心思想是把一次完成的大任务拆成多次完成的小任务。对 PDF 导出而言,小任务就是逐页或逐块调用 addPage:每块内容独立测量、独立排版,前一块的结果不影响后一块,任务边界清晰,失败时只需重试失败的那一块,而不是整份文档推倒重来,排查问题的范围也缩小到具体内容块。

分段构建还有个隐藏优势:单块内容的 DOM 规模小,快照与样式计算都更轻,内存峰值显著下降。对图表、长表格这类结构复杂的内容,单独成块还能精确控制分页位置,避免内容被从中间切断,排版质量反而比整页一次性渲染更可控,表格跨页断行的老大难问题也更容易处理。

适用场景上,增量渲染最适合内容按章节组织的长文档:合同条款、报表章节、说明书、多门店账单等。内容天然分块的场景几乎零成本接入;内容是一整段连续文本的,则先按标题或固定长度切片,再逐段 addPage,效果同样成立,核心是找到合适的分块粒度,粒度太小消息开销大,粒度太大又回到老问题。

代码示例:addPage 分段构建文档

下面的示例演示如何把一份长报告拆成多个内容块逐段构建:封面、正文各章节分别调用 addPage,最后统一保存。注意 addPage 接受 HTML 字符串或 DOM 元素,传入的内容会被渲染成页面,重复调用即实现多页文档,这是分段生成的最小可运行骨架,任何长文档导出都可以从这份骨架扩展而来。

实际项目中,分段逻辑通常封装成一个循环:遍历章节数据,为每个章节生成 HTML 片段并 addPage。章节数据来自接口或本地数组,HTML 片段由模板函数生成,这样导出逻辑与数据源解耦,新增章节不需要改导出代码,扩展性很好,内容结构变化时只改数据与模板,导出引擎本身纹丝不动。

分段构建时注意保持样式一致:每个内容块的 HTML 里都要带上公共样式,包括字体、边距、行高与页面边距设置,或者把公共样式抽成常量拼接进每一段,避免不同页面出现字体、间距不一致的观感问题。公共样式集中管理,改动一次全局生效,各页之间的视觉统一性就有了保障。

import { DomPDF } from 'dompdf.js';

const chapters = [
  { title: '第一章 概述', body: '<p>项目背景、目标与范围说明。</p>' },
  { title: '第二章 数据', body: '<table><tr><td>指标</td><td>数值</td></tr></table>' },
  { title: '第三章 结论', body: '<p>结论与后续建议。</p>' },
];

const pdf = new DomPDF();

// 封面页:独立 addPage
pdf.addPage('<h1 style="text-align:center">年度经营分析报告</h1>', { format: 'A4' });

// 逐章增量添加:每章一页
for (const chapter of chapters) {
  const html = `<h2>${chapter.title}</h2>${chapter.body}`;
  pdf.addPage(html, { format: 'A4' });
}

// 全部块就绪后统一保存
pdf.save('annual-report.pdf');

进度回调与可感知性能

长文档导出没有进度反馈,用户就只能干等,等待时间稍长就会怀疑页面卡死。dompdf.js 的渲染在 Worker 中分页进行,配合任务级进度统计,可以做到第 12/58 页这样的精确反馈:每完成一个内容块就累计一次进度,渲染全部完成后统一保存,进度条从 0 平滑走到 100%,等待体验完全不同。

实现上只需要一个计数器:总块数已知,每块完成加一,进度等于完成数除以总数。块的大小尽量均匀,进度条才走得均匀;如果某一块特别大,可以再把它拆细,让进度更新更平滑。进度更新用 requestAnimationFrame 节流,避免每块都触发重绘造成不必要的开销,进度展示本身不要成为性能负担。

进度反馈之外,还可以提供取消能力:用户在等待中发现内容有误,可以中止任务、释放资源、修改后再导出。取消按钮配合任务状态机,也就是待处理、进行中、已完成、已取消四种状态,是导出功能从能用走向好用的关键一步,实现成本不高,但对用户体验的提升非常明显,值得纳入第一版设计。

内存与资源优化

分段生成的内存优化重点是控制快照规模:单块内容的 HTML 保持精简,长表格拆成多个小块逐个添加,图片统一压缩后再嵌入,避免大图以原始尺寸塞进模板。每完成一块,及时释放对应的 DOM 引用与临时对象,让垃圾回收有机会工作,内存曲线保持平稳,长时间运行也不会持续膨胀。

对于图片密集的文档,建议先做图片预处理:超出页面宽度的图片等比缩放,位图统一转成体积更小的编码格式,装饰性图片直接移除。图片是 PDF 体积与内存的主要来源,预处理一步到位,后面的渲染、存储、传输全部受益,这个环节投入的精力回报率非常高。

字体也要纳入内存考量:自定义字体每份都是几 MB 到十几 MB,分段构建时整个文档共用同一份字体资源,不要每块重复加载。字体文件加载一次、全局复用,快照里不要内联重复的字体数据,否则分段越多重复开销越大,字体加载的等待时间也会随分段数量线性增长,得不偿失。

批量导出与任务队列

批量导出(一次生成多份 PDF)与单文档分段生成是两个层次的工程问题:前者是任务队列,后者是任务内部的执行方式。队列层负责调度,包括任务入队、串行或并发执行、失败重试、结果汇总;执行层用分段构建保证单任务的内存与速度。两层分离,各自演进,代码结构清晰,出问题时定位也快。

并发策略上,先串行后并行:默认一次处理一个任务,观察单任务耗时与内存占用,确认资源充裕后,再用少量并发(例如同时两个任务)提升吞吐。并发数不是越大越好,Worker 实例与 WASM 实例都会占用内存,压测找到平衡点,线上运行更稳,盲目提高并发只会让所有任务一起变慢。

队列还需要持久化:页面刷新后未完成的任务可以恢复,至少要把任务状态记录到本地存储或服务端。对批量导出这种可能耗时几分钟的操作,用户中途离开是常态,任务可恢复、完成后有通知,是这类功能能否被用户信任的关键,也是后台系统批量能力成熟度的重要标志。

常见问题与最佳实践

Q: 分段添加后页码顺序错乱?A: addPage 严格按照调用顺序生成页面,检查是否在异步回调里乱序调用;分段构建建议同步循环或串行 await,避免并发添加导致顺序不确定,必要时给每块加序号日志验证顺序,问题很快就能暴露。

Q: 文档很大时导出仍然慢?A: 先定位瓶颈:是 DOM 快照慢、字体加载慢还是渲染慢。快照慢就精简模板、减少节点数;字体慢就做子集化;渲染慢就进一步切分内容块、降低单块复杂度。用性能工具测量各阶段耗时,对症下药而不是盲目优化,方向对了优化才有效果。

最佳实践小结:内容按章节分块、逐块 addPage;公共样式抽常量统一拼接;进度按块统计并节流更新;图片预处理、字体全局复用;批量场景用任务队列串行起步。按这五条执行,大文档导出从碰运气变成可预期,千页文档也能稳定交付,团队对导出功能的信心会建立起来。

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。