懒加载实战:提升前端性能的高效加载策略
优化前:卡得不行
上周上线一个商品列表页,用户反馈“一划就卡”“图片加载半天不动”。我本地跑起来也确实离谱——首屏还没出来,浏览器已经卡到鼠标都转圈。打开 DevTools 一看,Network 里堆了 80+ 张高清图,全是 1MB+ 的大图,总加载时间飙到 5s 以上。更糟的是,这些图全在页面顶部一次性请求,连用户没看到的区域也一股脑塞进来,内存直接爆到 1.2GB,低端机直接白屏。
这哪是懒加载,这是“勤快过头”了。得赶紧治。
找到瓶颈了!
先用 Performance 面板录了一次滚动操作,结果很明显:主线程被大量图片解码和布局计算占满,FPS 掉到个位数。再看 Memory,JS heap 快速膨胀,说明 DOM 节点和图片资源没释放。问题核心就两点:
- 所有图片不管是否可见,全在 HTML 渲染时触发加载
- 图片尺寸太大,没做响应式压缩
其实第二点属于资源优化范畴,但懒加载做得好,能大幅减少不必要的请求,所以优先解决第一点。
试了几种方案,最后这个效果最好
一开始想用老办法:scroll 事件监听 + getBoundingClientRect 判断元素是否进入视口。写完一测,滚动稍微快点就漏判,而且频繁触发 scroll 事件导致性能更差。后来听说 Intersection Observer API 是为这事量身定做的,立马换上。
折腾半天发现,关键不是 API 本身,而是怎么用才稳。比如,不能只等元素完全进入视口才加载,那样用户会看到空白。得提前一点,比如 rootMargin: '50px',让图片在离视口还有 50px 时就开始加载。另外,记得加个防抖?其实不用,Intersection Observer 本身是异步的,不会阻塞主线程,反而比手动 throttle 更高效。
还踩了个坑:有些图片容器是动态插入的(比如分页加载),Observer 得重新 observe 新元素。一开始忘了这点,新加载的图根本不触发,查了半天才发现。
核心代码就这几行
下面是最简可运行的懒加载实现,亲测有效。注意几个细节:
- 用
data-src存真实图片地址,避免 img 标签默认加载 - 加载完成后移除
data-src,防止重复触发 - 给图片加个淡入效果,体验更顺滑
<img class="lazy" data-src="https://jztheme.com/images/product1.jpg" alt="商品1">
<img class="lazy" data-src="https://jztheme.com/images/product2.jpg" alt="商品2">
.lazy {
opacity: 0;
transition: opacity 0.3s ease;
}
.lazy.loaded {
opacity: 1;
}
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
img.removeAttribute('data-src');
observer.unobserve(img); // 加载完就取消监听,省资源
}
});
}, {
rootMargin: '50px' // 提前50px触发
});
images.forEach(img => imageObserver.observe(img));
这段代码在 Chrome、Safari、Firefox 都跑通了。对于不支持 Intersection Observer 的老浏览器(比如 IE11),我加了个简单 polyfill,或者直接降级为立即加载——毕竟现在用 IE 的用户几乎可以忽略,没必要为了 0.1% 的用户拖慢 99.9% 的体验。
别忘了图片本身也要优化
懒加载只是第一步。如果图片还是 2MB 一张,就算延迟加载,加载时也会卡顿。所以我顺手做了两件事:
- 后端按设备像素比返回不同尺寸(比如 1x、2x)
- 前端用
<picture>或srcset让浏览器自动选最优图
不过这不是本文重点,简单提一句。重点是懒加载配合响应式图片,效果才真正起飞。
性能数据对比
改完后重新跑 Lighthouse,数据变化很直观:
- 首屏加载时间从 5.2s 降到 800ms
- 初始请求数从 87 降到 12(只加载首屏可见图)
- 滚动 FPS 从平均 12 提升到 58
- 内存占用峰值从 1.2GB 降到 400MB
最爽的是,用户反馈“滑动流畅多了”,连产品都来问是不是换了新服务器(笑)。其实啥都没换,就是少加载了 70 多张没用的图。
还有个小问题没完美解决
目前方案在快速滚动到底部时,中间跳过的图片不会加载(因为 Intersection Observer 只在元素进入视口时触发一次)。虽然不影响功能,但用户如果回滚,那些图还是空白。理论上可以用 threshold: 0 让只要露出 1px 就触发,但实测在低端机上反而增加负担。权衡之后,我决定保留当前逻辑——毕竟绝大多数用户不会疯狂上下滚动,而且首屏和当前屏的体验已经足够好。完美主义害死人,实用主义救世界。
以上是我的优化经验,有更好的方案欢迎交流
懒加载看似简单,但细节很多。比如要不要预加载相邻区域?要不要结合骨架屏?这些都可以继续优化。但对我这个项目来说,Intersection Observer + 响应式图片这套组合拳已经够用。如果你有更轻量、更兼容的方案,或者遇到类似问题,欢迎评论区聊聊。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

暂无评论