业务系统里批量导出是刚需:月底要导出一整月几百张发票,HR 要给全员生成工资条,财务要做季度报表合集。传统方案把这些任务丢给后端,排队、等待、超时是家常便饭,高峰期甚至要通宵跑任务。dompdf.js 采用 Rust+WASM 内核,在浏览器端实测千页文档两秒左右即可完成渲染,配合循环 addPage 可以一键生成多页文档,配合循环 save 可以批量下载多个文件。本文给出完整的批量导出代码、性能与内存优化建议、进度提示等用户体验方案,并整理常见坑点,帮助你把批量导出功能做得又快又稳,让用户告别漫长的等待。
批量导出通常分为两种形态:把多条数据合成一个多页 PDF(如发票簿、报表合集),以及把每条数据分别导出为独立 PDF(如按员工生成工资条、按订单生成送货单)。两种形态对应不同的业务诉求,代码结构也完全不同。
两种形态的实现路径不同:合并在一个文档里用循环 addPage,最后 save 一次;拆分成多个文件则需要循环生成并逐个 save,浏览器会为每次 save 触发一次下载,两者的代码结构差异明显,需要分别设计。
选哪种形态取决于使用场景:需要归档留存的选合并文档,需要分发给不同对象的选独立文件。也可以两种都提供,让用户在界面上选择导出方式,满足不同角色的使用习惯,产品完整度更高。
从产品视角看,批量导出往往还附带格式要求:文件名规范、日期范围、导出范围筛选。建议在导出面板上把这些参数一次配置齐,前端生成时直接套用,减少人工二次整理,交付更规范。
导出权限也要考虑:不同角色能导出的数据范围不同,前端导出前先向接口确认权限与数据范围,后端再做一次兜底校验,双保险能避免越权导出,这是批量导出上线前必须检查的安全点。
导出前的二次确认也很重要:弹窗展示将要导出的数量与文件大小,让用户确认后再开始,既避免误操作,也让大任务有心理预期,减少中途取消造成的浪费。
import { DomPDF } from 'dompdf.js';
const invoices = [
{ no: 'INV-20260801', amount: '¥ 1,200.00' },
{ no: 'INV-20260802', amount: '¥ 3,450.00' },
// ... 数百条发票数据,从接口分页拉取后合并
];
const pdf = new DomPDF();
for (const inv of invoices) {
const pageHTML = `
<h2 style="text-align:center">发票 ${inv.no}</h2>
<table style="width:100%;border-collapse:collapse">
<tr><th style="border:1px solid #000;padding:8px">发票号</th>
<th style="border:1px solid #000;padding:8px">金额</th></tr>
<tr><td style="border:1px solid #000;padding:8px">${inv.no}</td>
<td style="border:1px solid #000;padding:8px">${inv.amount}</td></tr>
</table>
<div style="page-break-after: always"></div>`;
pdf.addPage(pageHTML, { format: 'A4', margin: '15mm' });
}
// 所有页渲染完成后一次性保存,生成完整的多页文档
pdf.save('invoices-2026-08.pdf');
import { DomPDF } from 'dompdf.js';
async function exportAll(employees) {
for (let i = 0; i < employees.length; i++) {
const emp = employees[i];
const slipHTML = `
<h1>${emp.name} 工资条</h1>
<p>基本工资:${emp.base} 绩效:${emp.bonus}</p>`;
const pdf = new DomPDF();
pdf.addPage(slipHTML, { format: 'A4' });
pdf.save(`${emp.name}-salary-2026-08.pdf`);
// 更新进度条,让用户看到实时进度
updateProgress(i + 1, employees.length);
// 让出主线程,避免页面卡死
await new Promise(r => setTimeout(r, 50));
}
}
exportAll(employeeList);
WASM 内核渲染极快,实测千页文档约两秒完成,瓶颈通常不在渲染而在数据准备与 DOM 字符串拼接。建议把数据先整理成数组,再统一模板化生成 HTML,避免在循环里做大量字符串拼接,拖慢整体速度。
导出多个独立文件时,循环里每次 new DomPDF() 都是独立实例,渲染完即可释放;配合 await setTimeout 让出主线程,页面滚动条和进度条都不会卡死,用户操作体验更好,页面始终保持响应。
大数据量下建议分批处理:每 50-100 条为一组,组间用 requestAnimationFrame 或 setTimeout 间隔,配合进度提示,用户体验会明显优于一次性暴力渲染,也更不容易触发浏览器内存压力,稳定性更高。
数据预处理也影响性能:把金额、日期等字段在循环外格式化好,避免每页模板里重复做格式化逻辑;模板里只做插值,渲染速度会明显提升,代码也更清晰,一举两得。
内存方面还有一个细节:循环里及时把不再使用的 HTML 字符串置空,避免大字符串滞留导致内存峰值过高;对超长列表尤其有效,页面稳定性更好,长时间导出也不容易卡顿。
进度提示的粒度也值得设计:条数少的导出按条更新即可,条数多的建议按百分比更新,避免进度条频繁重绘消耗性能。配合完成后的提示音或弹窗,体验会更完整。
批量导出最怕用户以为页面卡死。前端方案的优势在于可以实时反馈:循环里每完成一条就更新进度条百分比,用户能清楚看到进度,等待焦虑大幅下降,对大批量导出尤其重要。
浏览器对自动下载有限制(通常一次会话内多次下载会弹拦截提示),批量下载多个文件时建议提示用户允许本次多文件下载,或在界面上提供打包下载选项,把选择权交给用户,避免下载被静默拦截。
对超大批量(如上万条),可以考虑结合 Web Worker 或分页加载数据,先把用户选中的记录号传给导出逻辑,避免一次拉全量数据导致内存暴涨,也可以先让用户筛选范围再导出,从源头控制数据量。
批量导出后还可以提供结果清单:成功导出的文件数、被跳过的异常记录、耗时统计,让用户对结果一目了然,也便于问题追踪与责任界定,是成熟产品该有的细节。
文件名规范同样重要:建议统一为业务前缀加日期加序号,比如 invoices-2026-08-001.pdf,便于用户归档与检索;中文文件名在不同系统间的兼容性差异较大,必要时做兼容处理。
Q: 生成的 PDF 少了几页?A: 检查循环中是否漏掉 addPage,或数据源是否分页未取全;建议在循环外记录 addPage 调用次数并与数据长度比对,第一时间发现问题,避免交付错误文档。
Q: 浏览器拦截多次下载怎么办?A: 这是浏览器安全策略,合并成单个多页 PDF 可绕开;或引导用户开启站点自动下载权限,在帮助文档里说明原因,减少用户困惑。
Q: 千页以上的文档内存会爆吗?A: dompdf.js 按页流式处理,实测稳定;若数据量极端,可分批生成多个 PDF 再提示用户逐个保存,规避风险,不把所有鸡蛋放在一个篮子里。
Q: 每页都要页眉页脚页码?A: 用 @page 分页媒体属性统一配置,所有 addPage 的页面自动带上页眉页脚与页码,不需要每页重复编写,维护成本极低。
Q: 导出过程中用户切换页面会导致任务中断?A: 建议在导出开始时锁定关键操作或提供后台任务提示,并在完成后弹出下载链接,避免任务静默丢失,用户找不到文件。
Q: 能不能只导出用户勾选的部分记录?A: 完全可以,把勾选记录的主键数组传给导出函数即可,实现与全量导出完全一致,只是数据范围不同,前端不需要额外逻辑。
Q: 导出任务可以后台运行吗?A: 浏览器内的同步导出会占用主线程,长时间任务建议配合 Web Worker 拆分数据准备逻辑,渲染本身保持在前台,两者结合能兼顾响应与稳定。
Q: 大批量导出时提示内存不足?A: 降低单批条数、及时释放不再使用的字符串与实例,必要时把数据分页拉取,分而治之,大多数内存问题都能解决。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。