懒加载是什么?前端哪些资源适合懒加载?
简化版
懒加载是把资源的发现、下载或初始化从首屏推迟到接近需要时,适合非首屏图片、后续路由、低频弹窗、图表和编辑器。首屏 LCP 资源、进入页面马上要用的核心交互不应盲目延后;懒加载必须同时设计触发时机、加载态、失败重试、预取和旧 chunk 保留。
详细版
图片可用原生 loading="lazy",需要更细控制时用 IntersectionObserver;代码以动态 import() 形成异步 chunk,由路由或交互触发。数据“分页加载”和 DOM“虚拟化”也减少初始工作,但它们与资源懒加载不是同一个概念。
判断边界要问资源是否首屏必要、命中概率、体积和触发后可接受等待。高概率下一步可以在空闲、悬停或接近视口时提前加载,低概率大资源则保持真正按需。
动态导入要处理网络失败和部署版本切换。新版本若立即删除旧 hash 资源,已打开的旧页面在稍后导入时会 404。
完整版教学
一、懒加载是在时间轴上转移成本
它通常不会让总工作凭空消失,而是把“所有用户启动时支付”改为“真正使用的用户稍后支付”。若资源永远未用,成本被完全避免;若马上使用,只是从首屏变成交互等待。
假设 100 KB 编辑器只有 20% 用户打开,全部预载会产生每 100 次访问 10 MB 传输;按需加载理论上约 2 MB,减少 80%。但打开它的用户会多一次网络与初始化等待。
心法:懒加载不是免费删除成本,而是在命中率、首屏速度和后续等待之间重新分配成本。
二、图片原生懒加载与 IntersectionObserver
原生 loading="lazy" 由浏览器决定何时请求,代码少且能利用浏览器启发式,适合常规非首屏图片。IntersectionObserver 能在元素接近视口时触发自定义加载,适合需要占位、动画或特殊数据逻辑的场景。
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
loadCard(entry.target)
observer.unobserve(entry.target)
}
}, { rootMargin: '300px 0px' })
rootMargin 300px 表示在进入视口前预留加载距离,但不是时间保证:快速滚动和慢网下仍可能来不及。观察器回调也应轻量,不能在每个元素进入时执行大块同步工作。
三、动态 import 同时涉及分包和运行时触发
构建器看到可拆分的动态 import() 通常生成异步 chunk,运行时执行表达式才发起请求。若应用初始化时立刻调用它,虽然产物分包了,却没有真正延迟多久。
async function openChart() {
showSkeleton()
try {
const { renderChart } = await import('./chart.js')
renderChart()
} catch (error) {
showRetry(error)
}
}
路由、组件和重型能力是常见边界。拆分过细会形成“组件 chunk → 图表库 chunk → 样式/数据”的串行发现,HTTP/2/3 也不能消除运行时代码才揭示依赖的逻辑瀑布。
四、哪些资源不适合懒加载
首屏大图、关键 CSS、初始字体和立即可操作控件若延迟,会恶化 LCP、闪烁或交互。不能因为属性名叫 lazy 就认为浏览器一定做出符合业务的优先级选择。
| 资源 | 默认倾向 | 判断原因 |
|---|---|---|
| 首屏 LCP 图片 | eager/高优先级 | 决定主内容出现 |
| 屏下列表图片 | lazy | 首屏不可见 |
| 核心导航代码 | 首屏加载 | 立即交互需要 |
| 低频编辑器 | 动态导入 | 体积大、命中率低 |
页面首屏边界随视口和模板变化。桌面屏下资源可能在大屏成为首屏,所以要在多视口真实测试,而不是按 DOM 序号固定前 3 张图 eager。
五、预取怎样缓和用户触发后的等待
高概率路径可在首屏稳定后、链接悬停或进入视口时预取。预取是概率投资:命中时减少等待,未命中时浪费流量和缓存空间。
假设详情页 chunk 为 160 KB,1000 次列表访问中 700 次进入详情,预取命中率 70%;会多下载 160 MB,其中约 112 MB 被使用、48 MB 未使用。是否值得还要结合网络、停留时间和业务价值。
省流量模式、弱网和后台标签页应更保守。不要把 prefetch 当成低优先级就无限添加,浏览器资源调度和缓存分区也会影响实际收益。
六、失败、布局与发布是懒加载的一部分
图片和组件加载时应保留稳定占位,避免内容到达后推开布局。组件要显示可理解的 loading,超时或离线时给重试,而不是永久转圈。
| 风险 | 用户表现 | 处理 |
|---|---|---|
| 请求慢 | 交互后空白 | 即时加载态、提前触发 |
| chunk 404 | 功能打不开 | 保留旧资源、受控刷新 |
| 加载失败 | 无限等待 | 错误边界与重试 |
| 高度未知 | 页面跳动 | 宽高/骨架占位 |
发布时先上传新 hash 资源,再切换 HTML,并保留旧资源一段时间。这样已打开页面仍能完成未来动态导入,回滚也不会引用被删除文件。
七、常见误区与追问
- 误区:能懒加载的资源都应该懒加载。 立即需要的资源延迟后会把首屏问题变成交互问题。
- 误区:动态 import 一定意味着用户触发才下载。 真正请求时间由运行时调用时机决定。
- 误区:IntersectionObserver 能保证图片进入视口前加载完成。 它只通知相交状态,网络速度和滚动速度仍会影响结果。
- 追问:懒加载和代码分割有什么区别? 分割产生独立 chunk,懒加载决定何时请求;二者相关但不等价。
- 追问:怎样选择预取时机? 根据下一步概率、资源体积、网络状态和当前页面稳定程度制定预算。
- 追问:为什么部署后动态 import 偶发失败? 旧页面引用旧 hash,而服务端可能已清理对应 chunk。
- 追问:如何评价懒加载是否成功? 同时比较初始资源、LCP、触发等待、失败率和预取命中浪费。
八、加强记忆
把懒加载记成“移成本、选边界、早一点、兜失败”:把非关键成本从启动期移走,按命中率与用户路径选择图片、路由和组件边界;对高概率资源在真正需要前适度预取;最后用稳定占位、加载态、错误重试和旧 chunk 保留保证体验。首屏关键资源则明确排除在 lazy 之外。