用 IntersectionObserver 做图片懒加载与曝光统计:一次滚动性能优化实测

举报
yd_236964775 发表于 2026/11/08 08:55:55 2026/11/08
【摘要】 最近在优化一个商品列表页时遇到一个典型问题:页面一次渲染几十张商品图,首屏还没滑到下面的图片就已经全部发起了请求。结果首屏关键图片反而要排队,加载完成时间被拖长,手机上流量也白白浪费。排查之后,我把图片加载改成了 IntersectionObserver 方案,顺带把曝光统计也接了进来,效果很明显。 为什么不用 scroll 监听最常见的懒加载写法是监听 scroll 事件,再用 getBo...

最近在优化一个商品列表页时遇到一个典型问题:页面一次渲染几十张商品图,首屏还没滑到下面的图片就已经全部发起了请求。结果首屏关键图片反而要排队,加载完成时间被拖长,手机上流量也白白浪费。排查之后,我把图片加载改成了 IntersectionObserver 方案,顺带把曝光统计也接了进来,效果很明显。

为什么不用 scroll 监听

最常见的懒加载写法是监听 scroll 事件,再用 getBoundingClientRect 逐个判断图片是否进入视口。这种写法有两个硬伤:第一,scroll 事件触发非常频繁,即使加了节流,每次回调里遍历几十个节点做矩形计算,依然会占用主线程,低端机上滚动会有顿挫感;第二,判断逻辑要自己处理节流时间、边界条件,稍不注意就会出现图片迟迟不加载或者重复加载。

IntersectionObserver 把“元素是否可见”这个判断交给了浏览器。它在渲染流水线里异步计算交叉状态,只有当目标元素的可见状态发生变化时才触发回调,不会跟着滚动事件高频执行,也不会打断滚动本身的流畅性。对开发者来说,只需要关心“可见了之后做什么”,代码反而更简单。

懒加载的核心写法

思路很直接:图片初始用一张轻量占位图,真实地址放在 data-src 里;用 observer 监听所有待加载图片,一旦进入视口就把 data-src 换成 src,然后解除监听。

const io = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;
    const img = entry.target;
    img.src = img.dataset.src;
    io.unobserve(img);
  });
}, {
  rootMargin: '200px 0px',
  threshold: 0
});
document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));

rootMargin 设为 200px 很关键:它把“进入视口”的判定范围提前放宽,图片在用户滑到之前就开始加载,滑到时通常已经完成,视觉上没有从空白到图片的闪烁感。加载完成后立即 unobserve,避免重复触发,也减少 observer 的持续开销。

同一个 API 顺手做曝光统计

曝光统计的需求和懒加载几乎一样:知道某个卡片是否被用户真正看到过。以前的做法同样是 scroll 事件里算位置,容易重复上报、也容易漏报。用同一个 observer、把 threshold 设为 0.5,再配合一个定时器,就能判断“可见面积超过一半且停留超过 1 秒”才算一次有效曝光。

const timers = new Map();
const statIo = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    const id = entry.target.dataset.id;
    if (entry.isIntersecting) {
      timers.set(id, setTimeout(() => {
        navigator.sendBeacon('/api/expose', JSON.stringify({ id }));
        statIo.unobserve(entry.target);
      }, 1000));
    } else {
      clearTimeout(timers.get(id));
    }
  });
}, { threshold: 0.5 });

元素滑出视口时清掉定时器,这样快速划过的卡片不会被误算;上报后立即解除监听,避免重复统计。用 sendBeacon 发送,即使页面马上被关闭,上报请求也不会被轻易丢掉。

实测效果与三个坑

在我的测试页面上改完之后,首屏图片请求从 42 个降到 6 个,LCP 从 3.8 秒降到 1.9 秒左右,滚动时的帧率也稳定了不少。过程中踩到的三个坑值得记下来:

第一,图片容器一定要预留高度。可以用 aspect-ratio 或固定宽高比占位,否则图片加载完成后撑高页面,会把下面的内容顶下去,既影响体验,也会抬高 CLS 指标。

第二,动态新增的节点要重新 observe。如果列表是下拉加载更多,新渲染出来的图片不会自动进入观察列表,需要在数据更新后重新调用 observe。

第三,低版本浏览器需要降级。IntersectionObserver 在现代浏览器里支持很好,但如果要兼容旧环境,准备一个 scroll 兜底分支是更稳妥的做法。

小结

IntersectionObserver 的价值在于把“可见性判断”这件事从主线程的事件回调里抽了出来:懒加载、曝光统计、无限滚动这些看似不同的需求,背后都是同一个“元素进入视口时触发一次回调”的模型。先想清楚触发条件,再决定回调里做什么,代码会比到处绑 scroll 监听清爽得多。下次再遇到长列表页面,不妨先从这个 API 开始排查。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。