← dompdf.js Studio

dompdf.js 大文档渲染性能优化实战指南

报表、合同、说明书、产品手册——几十页甚至上百页的大文档,是 PDF 生成性能压力的集中体现。页面多、图片多、样式复杂,三者叠加会让生成耗时从秒级飙升到分钟级,用户等待焦虑,服务器资源吃紧。dompdf.js 的 Rust+WASM 内核在浏览器中完成快照、布局、渲染与打包,性能上限很高,但实际表现高度依赖调用方的写法:模板复杂度、图片处理、分页策略、实例复用,每一个环节都直接影响最终耗时。本文从大文档的性能瓶颈分析入手,介绍分页策略与 scale 参数的取舍、批量生成的代码模式、图片压缩与降采样、样式精简与 DOM 瘦身,最后给出性能基准测试的方法与优化清单,帮助开发者把百页文档的生成时间控制在可接受的范围内。

大文档的性能瓶颈在哪里

大文档的耗时主要花在三个环节:DOM 快照与样式计算、图片解码与重采样、WASM 端的布局与渲染。页面越多,布局与渲染的累计成本越高;图片越大越多,解码与内存占用越突出;样式越复杂,快照与计算的单页成本越可观,三者相互叠加,形成大文档特有的性能瓶颈。

识别瓶颈不能靠猜,要量化:用 performance.now() 分别记录快照、生成与保存各阶段耗时,对比不同页面数与图片数下的时间曲线,很快就能看出耗时随哪个因素增长最快,针对性优化才有效果。盲目优化往往事倍功半,先测量再动手是性能工作的第一原则。

还有一个常被忽略的因素是主线程阻塞:生成期间页面交互卡顿,用户感知的等待时间远大于实际耗时。dompdf.js 的 WASM 管线在 Worker 中执行,主线程可以保持响应,但快照与资源准备仍在主线程,把这两部分也做轻,整体体验才真正流畅,用户才不会觉得页面被冻结。

分页策略:自动分页与手动控制

dompdf.js 支持自动分页:内容超过一页时自动拆分到下一页,连续内容用一次 addPage 即可。自动分页适合流式文档,正文长文直接交给引擎处理;但对表格、卡片这类不希望被拦腰截断的块,建议用 CSS 的 break-inside: avoid 控制拆分点,让每个块完整落入单页,观感更专业。

对版式要求高的文档,可以手动分页:把内容按业务逻辑切分成多个页面块,逐个 addPage,每页的起止完全可控。手动分页的代价是要自己处理内容划分,收益是每页版式稳定、可精确控制页数,适合合同、票据、证书这类强版式场景,交付结果与设计稿一致。

混合策略最实用:正文走自动分页,特殊块用 break-inside 约束,关键页面手动切分。无论哪种策略,都建议在真实内容上验证分页结果,确认没有内容被截断、块没有跨页断裂。分页正确性是文档质量的底线,比性能更优先,先保证正确再谈优化。

代码示例:批量分页生成大文档

批量生成的标准模式是循环 addPage:一个 DomPDF 实例依次接收多个页面内容,最后统一 save 输出。实例复用避免了重复初始化开销,页面间共享同一套渲染上下文;addPage 返回 Promise,循环内 await 即可顺序推进,不会并发抢占资源,行为可预期。

分批推进还方便做进度反馈:每完成一页更新一次进度,用户能看到生成进展。页面内容较多时,可以先把所有页面 HTML 准备好,再统一渲染;页面内容需要动态生成时,则逐页生成逐页提交。两种顺序按内容复杂度灵活选择,代码结构都很清晰。

下面示例演示循环生成十页文档:每页是一段模板字符串,循环内 await pdf.addPage,最后统一 save。模板中使用 CSS @page 的 margin 盒子配合 counter(page) 与 counter(pages) 生成页脚页码,多页文档的页码自动递增,无需手动计算,输出即专业。

import { DomPDF } from 'dompdf.js';

const pdf = new DomPDF({ format: 'A4', margin: '20mm' });

for (let i = 1; i <= 10; i++) {
  const html = `
    <style>
      @page { @bottom-center { content: counter(page) ' / ' counter(pages); } }
      body { font-family: 'Source Han Sans SC', sans-serif; }
    </style>
    <h2>第 ${i} 章</h2>
    <p>这是第 ${i} 页的内容,由循环批量生成。</p>`;
  await pdf.addPage(html, { format: 'A4' });
  console.log('page', i, 'done');
}

pdf.save('report-10pages.pdf');

图片压缩与降采样策略

大文档的图片策略直接决定体积与内存:一张 4000×3000 的照片解码后约占 48MB 内存,十张就是近 500MB,任何渲染管线都扛不住。原则是先降采样再嵌入:图片尺寸超过显示尺寸两倍以上的,用 canvas 缩放到显示尺寸附近,视觉无损失,内存与体积却大幅下降。

格式选择同样重要:照片统一转 JPEG 并控制压缩质量,界面截图用 PNG 保留清晰度,透明素材保持 PNG。批量场景可以写一个预处理函数,统一完成解码、缩放、压缩、编码,输出的图片直接交给模板,生成链路又快又稳,图片处理逻辑也集中在一处便于维护。

还要避免重复加载:同一张图片在多页出现时,只加载一次并复用;模板中引用同一地址,浏览器与渲染管线会命中缓存。图片资源的缓存策略(URL 稳定、Cache-Control 合理)在高频生成场景收益显著,值得在部署阶段就规划,让重复导出越来越快。

样式精简与 DOM 瘦身

样式复杂度直接影响每页的布局成本:深层嵌套的 flex/grid、大量阴影与滤镜、超大样式表,都会拖慢渲染。大文档建议用扁平化结构:减少不必要的嵌套层级,能用简单块布局就不用复杂弹性布局,样式规则按需声明,避免整份设计系统样式表直接套用,只带真正用到的部分。

DOM 瘦身同样有效:模板中只保留最终要呈现的内容,隐藏元素、调试节点、重复结构在生成前移除;长列表分页渲染,而不是一次渲染全部数据。DOM 越小,快照越快,WASM 端布局也越省,这是所有性能优化里性价比最高的一步,任何文档都适用。

动态数据要防止样式抖动:生成前固定字体、图片尺寸与行高,避免渲染过程中布局变化导致重复排版。把模板视为稳定的渲染输入,数据只负责填充,样式只负责呈现,输出结果可复现、耗时也可预期,调优才有基准可依,不会出现这次快下次慢的波动。

样式精简的收益在页面数量上呈线性放大:每页省下毫秒级的布局时间,一百页就是数百毫秒;配合 DOM 瘦身与图片优化,整体耗时往往能下降一半以上。建议把模板性能检查纳入代码评审,新模板上线前先跑一次基准,把性能问题挡在发布之前,而不是上线后救火。

性能基准与优化清单

优化前先建立基准:固定测试内容(如一百页、五十张图片的文档),记录生成耗时、PDF 体积与内存峰值,作为后续所有优化的对照。基准要在同一浏览器与机器上跑,控制变量,数据才可比;每次优化后重跑基准,用数据确认收益,而不是凭感觉判断效果。

优化清单按收益排序:先做 DOM 瘦身与样式精简(零成本、收益大),再做图片降采样与格式统一(成本低、收益明显),最后调整 scale 与分页策略(影响清晰度与版式,需权衡)。每项优化后检查输出质量,确保性能提升不以内容质量为代价,质量永远优先。

scale 参数是清晰度与性能的杠杆:scale 提高一倍,渲染像素量增加四倍,大文档耗时与内存随之上升。建议默认 1 到 1.5,仅在需要高清输出时提高,或者提供导出选项让用户按需选择,把性能成本交给需要的人,而不是所有用户一起承担,体验与性能兼顾。

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

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

Hello from dompdf.js!

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