IntersectionObserver实战:高效实现懒加载与可视区域检测

シ自乐 前端 阅读 1,696
赞 15 收藏
二维码
手机扫码查看
反馈

核心代码就这几行

别被名字吓到,IntersectionObserver 用起来其实特别简单。我第一次用它做懒加载图片的时候,核心逻辑就不到 10 行。

IntersectionObserver实战:高效实现懒加载与可视区域检测

先看最基础的用法:监听某个元素是否进入视口。比如页面里有一堆图片,一开始只放占位图,等用户滚动到附近再加载真实图片。亲测有效,比以前用 scroll 事件 + getBoundingClientRect 算位置稳多了,性能也更好。

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img); // 加载完就取消监听,省点资源
    }
  });
});

// 所有带 data-src 的 img 都交给它管
document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

HTML 长这样:

<img data-src="https://jztheme.com/real-image.jpg" alt="示例图" />

就这么简单。你甚至不用关心滚动事件、节流、防抖这些破事,浏览器原生帮你搞定。而且它在主线程之外运行,不会卡 UI,这点比手写 scroll 监听强太多。

这个场景最好用

除了懒加载,我还用它做过「无限滚动」和「组件曝光埋点」。

比如商品列表,用户快滚到底部时自动加载下一页。关键不是“到底了”,而是“快到底了”——这时候提前加载,体验更顺滑。靠 thresholdrootMargin 就能轻松实现。

const options = {
  rootMargin: '0px 0px 200px 0px', // 提前 200px 触发
  threshold: 0
};

const sentinel = document.querySelector('#sentinel'); // 页面底部的一个空 div
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMoreData(); // 自己封装的加载函数
  }
}, options);

observer.observe(sentinel);

埋点更简单。只要元素出现在屏幕上(哪怕只露 1 像素),就上报。我们之前用这个统计 Banner 位、广告位的曝光率,准确率比 scroll 计算高不少,因为不会漏掉快速滚动跳过的区域。

const trackObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 上报 entry.target.id 或其他标识
      fetch('https://jztheme.com/api/track', {
        method: 'POST',
        body: JSON.stringify({ elementId: entry.target.id })
      });
      // 注意:这里一般不 unobserve,因为可能多次进出
    }
  });
}, { threshold: 0.1 }); // 至少露出 10% 才算曝光

document.querySelectorAll('.track-item').forEach(el => {
  trackObserver.observe(el);
});

踩坑提醒:这三点一定注意

我踩过好几次坑,这里重点说三个。

第一,别忘了处理 SSR 或服务端渲染的兼容问题。 如果你在 Next.js、Nuxt 这类框架里用,记得把 new IntersectionObserver 包在 useEffectmounted 里,或者加个 typeof window !== 'undefined' 判断。不然服务端跑会直接报错,因为 Node 环境没有 IntersectionObserver

第二,unobservedisconnect 别乱用。 如果你只监听一次(比如懒加载),记得在回调里调 unobserve(target),避免重复触发。但如果你要持续监听(比如埋点),就别 unobserve,否则第二次进入视口就收不到通知了。另外,页面销毁时记得 observer.disconnect(),防止内存泄漏——虽然现代浏览器 GC 很强,但养成习惯不吃亏。

第三,rootMargin 的单位必须带 px,不能用百分比或 rem。 我之前想用 rootMargin: '10%',结果完全没反应,折腾半天才发现规范只认像素值。文档里写得挺清楚,但我就是没细看,血泪教训。

高级技巧:动态调整阈值

有时候你可能需要同一个元素在不同阶段触发不同行为。比如一个视频组件,露出 10% 时预加载,露出 50% 时开始播放,露出 90% 时上报深度曝光。这时候可以复用同一个 observer,靠判断 intersectionRatio 来分层处理。

const videoObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const ratio = entry.intersectionRatio;
    const video = entry.target;

    if (ratio >= 0.1 && !video.preloaded) {
      video.preload = 'auto';
      video.preloaded = true;
    }

    if (ratio >= 0.5 && !video.played) {
      video.play();
      video.played = true;
    }

    if (ratio >= 0.9) {
      // 上报深度曝光
    }
  });
}, { threshold: [0.1, 0.5, 0.9] }); // 关键:传入多个阈值

videoObserver.observe(document.querySelector('#my-video'));

注意 threshold 要写成数组,这样每次跨越任一阈值都会触发回调。不过要小心频繁触发,尤其是用户来回滚动时。如果业务允许,可以在状态标记后忽略后续相同阶段的触发(比如上面的 preloadedplayed 标志位)。

最后说两句

IntersectionObserver 真的是前端性能优化的利器,尤其在长列表、懒加载、动画触发这些场景下,代码简洁又高效。虽然有些老项目还在用 scroll + getBoundingClientRect,但新项目我建议直接上它。

当然,它也不是万能的。比如你需要精确知道元素滚动了多少像素,或者要做复杂的视差效果,那还是得回到 scroll 事件。但 80% 的“是否可见”类需求,它都能优雅解决。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如配合 CSS 动画、做虚拟列表的简化版),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论