背景
在管理后台系统中,报表导出是常见的需求。当我们将复杂DOM结构转换为PDF时,传统方案(如直接使用jsPDF)在会导致主线程阻塞,造成页面卡顿。本文介绍如何通过Web Worker优化这一过程。
技术选型
- html2canvas:将DOM转换为Canvas
- jsPDF:生成PDF文件
- Web Worker:避免主线程阻塞
实现方案对比
传统方案痛点
- 同步操作阻塞UI渲染
- 复杂DOM处理时卡顿明显
传统方案性能图:

Web Worker方案优势
- 将CPU密集型操作转移到独立线程
- 保持主线程响应流畅
web worker方案性能图:

核心技术分析
html2canvas:DOM 到 Canvas 的桥梁
技术定位 :基于纯前端实现的 DOM 渲染捕获库
核心价值 :
将任意 HTML 元素(包括复杂布局、CSS 样式)无损转换为 Canvas 图像,解决「可视化内容转文件」的关键第一步。
应用要点:
- 需处理跨域资源(
useCORS: true
)和滚动区域捕获(scrollY: -scrollTop
) - 注意浏览器兼容性(如 CSS 特性支持度)
- 性能敏感(大尺寸 DOM 转换需优化)
jsPDF:客户端 PDF 生成引擎
技术定位 :浏览器端动态创建 PDF 文件的轻量级解决方案
核心价值 :
摆脱服务端依赖,实现纯前端 PDF 文件构建,支持图片嵌入、多页排版等基础功能。
应用要点:
- 需动态计算页面尺寸(如 A4 比例适配
[210mm, (height*210)/width]
) - 字体管理需特殊处理(示例中使用图片规避字体嵌入问题)
- 建议 CDN 动态加载(减少主包体积)
Web Worker:浏览器多线程实践
技术定位 :浏览器环境的多线程并行计算能力
核心价值 :
将 CPU 密集的 PDF 生成逻辑与主线程解耦,避免 UI 冻结,提升用户体验。
应用要点:
- 采用 Blob 动态构造 Worker(规避独立文件维护成本)
- 需严格管理生命周期(
terminate()
+revokeObjectURL
防内存泄漏) - 线程间通信需序列化数据(示例中通过
ArrayBuffer
传输二进制)
技术联动关系

三者形成 「DOM捕获 → 后台生成 → 结果回传」 的完整链路,各司其职的同时通过 Worker 实现性能解耦。
核心实现步骤
1. DOM转Canvas处理
less
const canvas = await html2canvas(ref.current, {
height: scrollHeight,
width: scrollWidth,
useCORS: true, // 处理跨域资源
scrollY: -originalScrollTop // 捕获完整滚动区域
});
2. 动态创建Web Worker
ini
const createWorkerBlob = (): Blob => {
const workerCode = `
importScripts('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
// ...Worker逻辑
`;
return new Blob([workerCode], { type: "application/javascript" });
};
3. Worker线程处理PDF生成
ini
self.onmessage = async (e) => {
const pdf = new jsPDF();
pdf.addImage(...);
const pdfData = pdf.output('arraybuffer');
self.postMessage({ success: true, pdfData });
};
4. 主线程通信处理
ini
const worker = new Worker(workerUrl);
worker.onmessage = (e) => {
// 处理生成结果
const link = document.createElement("a");
link.download = filename;
link.click();
};
// 异常处理
worker.onerror = (e) => {
throw new Error("PDF生成失败: Worker错误");
};
效果对比
指标 | 传统方案 | Worker方案 |
---|---|---|
主线程阻塞时间 | 2.73s | - |
用户感知卡顿 | 明显 | 无 |
全部代码
ini
// import { jsPDF } from "jspdf";
import html2canvas from "html2canvas";
// 定义Worker返回的数据类型
interface WorkerSuccessResult {
success: true;
pdfData: ArrayBuffer;
}
interface WorkerErrorResult {
success: false;
error: string;
}
type WorkerResult = WorkerSuccessResult | WorkerErrorResult;
/**
* 创建包含 Web Worker 代码的 Blob,用于通过 `jsPDF` 生成 PDF。
* TODO:目前是引入官方cdn的jsPDF库,后续应该把文件转移到内部cdn库
*
* @returns {Blob} 包含 Web Worker 代码的 Blob。
*/
const createWorkerBlob = (): Blob => {
const workerCode = `
self.onmessage = async function(e) {
const { imgData, imgWidth, imgHeight, filename } = e.data;
try {
// 动态导入jsPDF库 (Worker内需要重新导入)
importScripts('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
// 创建PDF
const pdf = new self.jspdf.jsPDF('p', 'mm', [imgWidth, imgHeight]);
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
// 生成PDF数据
const pdfData = pdf.output('arraybuffer');
// 将生成的PDF数据返回给主线程
self.postMessage({ success: true, pdfData });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
`;
return new Blob([workerCode], { type: "application/javascript" });
};
/**
* 从指定的 React `RefObject` 引用的 HTML 元素生成 PDF,并触发下载。
*
* @param {React.RefObject<HTMLDivElement>} ref - 要转换为 PDF 的 HTML 元素的 React 引用。
* @param {string} [filename="document.pdf"] - 生成的 PDF 文件的名称。
*
* @throws 如果 PDF 生成失败,将抛出错误。
*/
export const printRefAsPDF = async (
ref: React.RefObject<HTMLDivElement>,
filename: string = "document.pdf"
) => {
if (!ref.current) return;
try {
// 保存原始滚动位置
const originalScrollTop = ref.current.scrollTop;
const originalScrollLeft = ref.current.scrollLeft;
// 获取元素实际总高度(包括溢出部分)
const scrollHeight = ref.current.scrollHeight;
const scrollWidth = ref.current.scrollWidth;
// 使用html2canvas捕获元素,设置高度为实际滚动高度
const canvas = await html2canvas(ref.current, {
height: scrollHeight,
width: scrollWidth,
scrollY: -originalScrollTop,
scrollX: -originalScrollLeft,
windowHeight: scrollHeight,
windowWidth: scrollWidth,
useCORS: true,
allowTaint: true,
logging: false,
});
const imgData = canvas.toDataURL("image/png");
// 计算PDF尺寸(A4纸比例)
const imgWidth = 210; // A4宽度(mm)
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 恢复原始滚动位置
ref.current.scrollTop = originalScrollTop;
ref.current.scrollLeft = originalScrollLeft;
// 创建Web Worker进行PDF生成
const workerBlob = createWorkerBlob();
const workerUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
// 等待Worker处理结果
const workerResult = await new Promise<WorkerResult>((resolve, reject) => {
worker.onmessage = (e) => resolve(e.data as WorkerResult);
worker.onerror = (e) => reject(new Error("PDF生成失败: Worker错误"));
worker.postMessage({ imgData, imgWidth, imgHeight, filename });
});
// 释放Worker资源
worker.terminate();
URL.revokeObjectURL(workerUrl);
// 处理Worker返回结果
if (workerResult.success) {
// 创建Blob链接并下载
const pdfBlob = new Blob([workerResult.pdfData], { type: "application/pdf" });
const pdfUrl = URL.createObjectURL(pdfBlob);
const link = document.createElement("a");
link.href = pdfUrl;
link.download = filename;
link.click();
// 清理URL
setTimeout(() => URL.revokeObjectURL(pdfUrl), 100);
} else {
throw new Error(workerResult.error || "生成PDF时发生错误");
}
} catch (error) {
// console.error("PDF生成失败", error);
throw new Error("PDF生成失败");
}
};
// 初始版本,但是转化为pdf的时候会导致主线程卡顿
// export const printRefAsPDFLegacy = async (
// ref: React.RefObject<HTMLDivElement>,
// filename: string = "document.pdf"
// ) => {
// if (!ref.current) return;
// try {
// // 保存原始滚动位置
// const originalScrollTop = ref.current.scrollTop;
// const originalScrollLeft = ref.current.scrollLeft;
// // 获取元素实际总高度(包括溢出部分)
// const scrollHeight = ref.current.scrollHeight;
// const scrollWidth = ref.current.scrollWidth;
// // 使用html2canvas捕获元素,设置高度为实际滚动高度
// const canvas = await html2canvas(ref.current, {
// height: scrollHeight,
// width: scrollWidth,
// scrollY: -originalScrollTop,
// scrollX: -originalScrollLeft,
// windowHeight: scrollHeight,
// windowWidth: scrollWidth,
// useCORS: true,
// allowTaint: true,
// logging: false,
// });
// const imgData = canvas.toDataURL("image/png");
// // 计算PDF尺寸(A4纸比例)
// const imgWidth = 210; // A4宽度(mm)
// const imgHeight = (canvas.height * imgWidth) / canvas.width;
// // 创建PDF,设置高度为内容的实际高度
// const pdf = new jsPDF("p", "mm", [imgWidth, imgHeight]);
// // 添加内容到PDF
// pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
// // 恢复原始滚动位置
// ref.current.scrollTop = originalScrollTop;
// ref.current.scrollLeft = originalScrollLeft;
// // 保存PDF
// pdf.save(filename);
// } catch (error) {
// console.error("PDF生成失败", error);
// }
// };
总结
通过Web Worker方案,我们成功将PDF生成的耗时操作转移到独立线程,在保证功能完整性的同时显著提升了用户体验。这种模式也适用于其他CPU密集型前端操作,如图像处理、复杂计算等场景。