懒加载是网页性能优化的标配:图片在滚动进入视口前不下载,页面首屏更快、流量更省。但懒加载与 PDF 生成天然冲突——生成 PDF 时,那些还没进入视口的图片可能尚未下载,模板一渲染就出现大片空白,导出结果与网页观感完全不符。dompdf.js 渲染真实 DOM,加载状态完全取决于页面当时的资源情况,因此开发者必须主动管理图片加载时序:要么在生成前强制加载所有图片,要么等待图片解码完成后才开始渲染。本文系统讲解懒加载与 PDF 生成的冲突根因、IntersectionObserver 与 loading 属性的陷阱、图片解码等待的正确写法、分批渲染与进度反馈的实现,以及错误处理与最佳实践,让导出 PDF 时每一张图片都稳定呈现。
懒加载的核心是延迟:图片在接近视口时才发起网络请求。页面展示时这是优点,但用户点击导出时,长页面下方的大量图片可能从未被请求过,DOM 里只有占位符或空壳,dompdf.js 快照这张 DOM 时,自然只能渲染出空白区域,导出的 PDF 就缺图了,这是最常见的缺图原因。
即使图片已经开始加载,异步网络请求与快照之间也存在竞态:快照发生在图片解码完成之前,同样拿不到像素数据。懒加载把这种竞态放大了——正常页面图片早已就绪,懒加载页面在导出瞬间往往只有首屏图片可用,其余全部悬空,随机性很强,复现问题都困难。
理解根因后,解决方案的方向就清晰了:导出前把模板用到的图片全部强制加载完成。无论页面平时怎么懒加载,导出流程必须走一遍完整的资源就绪门控,确保快照发生时所有图片都可读,这是懒加载场景下 PDF 不缺图的第一原则,也是所有方案的共同基础。
HTML 的 loading=lazy 属性和 IntersectionObserver 是两种常见的懒加载实现:前者由浏览器接管,后者由业务代码控制。共同点是图片只有进入视口才下载。导出 PDF 时,模板中未被观察或未进入视口的图片就停留在未加载状态,直接渲染必然缺图,需要在导出前统一处理。
另一个容易被忽略的细节是容器滚动:模板被放入不可见容器或 iframe 时,IntersectionObserver 可能认为所有图片都在视口外,一张都不加载。排查时先确认模板渲染的可见性与图片的实际请求情况,在控制台网络面板里看图片是否发出请求,避免在错误的方向上反复尝试。
结论不是禁用懒加载,而是把懒加载限制在页面浏览场景:模板用于 PDF 生成时,统一走预加载流程。把模板与页面解耦,页面该懒加载就懒加载,导出时用独立的、全量加载的模板副本,两条路径互不干扰,是工程上最干净的做法,也最容易维护。
标准的资源就绪门控分两层:先确保所有图片的加载与解码完成,再调用 addPage。图片元素提供了 decode() 方法,它返回的 Promise 在图片解码完成后 resolve,配合 Promise.all 可以一次性等待全部图片,比监听 load 事件更简洁可靠,也更贴近渲染管线的真实需求。
如果模板里既有 img 也有 CSS 背景图,仅遍历 img 元素不够,还需要检查背景图资源的加载状态。背景图可以用 Image 对象预加载同一地址,或者把背景图先转成 data URL 再放入样式,让模板自包含,加载状态完全由自己掌控,逻辑更简单,排查也更容易。
下面的代码遍历模板中的所有图片,等待它们全部解码后再交给 dompdf.js:querySelectorAll 收集图片,Promise.all 批量等待 decode,全部完成后创建 DomPDF 实例并 addPage。若某张图片解码失败,catch 里记录日志并继续,避免单张坏图阻塞整个导出流程,健壮性优先。
import { DomPDF } from 'dompdf.js';
async function waitForImages(root) {
const images = Array.from(root.querySelectorAll('img'));
await Promise.all(images.map(img => {
if (img.complete && img.naturalWidth > 0) return Promise.resolve();
return img.decode().catch(() => {});
}));
}
const html = `
<style>body { font-family: 'Source Han Sans SC', sans-serif; }</style>
<h2>产品图册</h2>
<img src="images/p1.jpg" alt="产品一">
<img src="images/p2.jpg" alt="产品二">`;
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
await waitForImages(container);
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(container, { format: 'A4' });
pdf.save('gallery.pdf');
大量图片一次性等待会让用户面对无反馈的等待,体验很差。工程上可以把导出拆成多步:先统计图片总数,再逐张或分批预加载,每完成一批就更新进度条,全部就绪后进入渲染阶段。整个过程用户能清晰感知进度,长任务不再像卡死,心理体验和实际体验都更好。
进度反馈的实现很简单:记录已加载数量与总数,加载完成时回调更新 UI;还可以把预加载与生成分成两个阶段,先显示加载进度,再显示渲染进度。dompdf.js 的 addPage 返回 Promise,可以逐个页面推进并更新进度,与加载阶段共用一套进度展示,代码结构统一。
分批的粒度需要权衡:一次加载全部图片内存占用高,逐张加载又太慢。建议按需分批,每批 10 到 20 张,既控制瞬时内存,又不会因为批次过多拖慢整体速度;图片特别大的场景还可以在批次间让出主线程,避免长时间阻塞页面交互,让用户能随时取消或重试。
懒加载页面里常见占位图与失败图:占位图是真实的图片资源,加载后会被替换;失败图是 onerror 替换的破图图标。PDF 生成时必须区分二者:占位图要等真实图加载完成,失败图要么重试、要么用占位替代,不能把破图直接写进交付文档,交付质量是底线。
错误处理建议走重试机制:图片加载失败后延迟重试一到两次,网络抖动场景下成功率显著提升;重试仍失败的图片记录日志并在模板中替换为占位样式,让 PDF 保持完整版式而不是留下破图区域。交付给用户的内容始终可用,即使个别图片失败也不会破坏整体观感。
重试要注意并发与时限:多张图片同时重试可能再次打满带宽,建议限制并发数;整体等待设置超时上限,超时后按失败处理进入渲染,避免用户无限等待。把重试、并发、超时封装成统一的预加载工具,页面与导出共用,逻辑单一、维护成本低,是值得投入的公共能力。
综合以上,懒加载场景的最佳实践可以归纳为四条:模板与页面解耦,导出用独立模板;导出前强制预加载所有图片并等待解码;预加载带进度反馈、错误重试与超时兜底;addPage 之前用资源就绪门控统一把关。四条都做到,缺图问题基本绝迹,导出结果稳定可复现。
性能上还有两个技巧:图片预加载时可以并行请求,但注意浏览器对同域名并发连接数有限制,必要时做并发控制;生成完成后及时释放大图引用与 Blob URL,避免内存长期占用,尤其是用户反复导出的场景,内存回收直接影响页面稳定性,不能忽略。
最后提醒一点:把预加载逻辑做成可复用的函数并纳入团队公共库,新项目直接复用,而不是每次重写。图片加载问题是 PDF 生成最高频的故障源之一,规范化的预加载流程能让整个团队受益,导出质量稳定,用户投诉也随之减少,长期维护成本显著降低。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。