IntersectionObserver 实战:曝光、懒加载、目录高亮和无限列表
本文目录
IntersectionObserver 让浏览器异步报告目标与视口/祖先容器的交叉变化,适合判断“接近或进入可视区域”。它不承诺精确像素重叠,也不是任意滚动动画的替代品;需要精确几何值时应使用合适的布局/API,而不是试图把 threshold 当标尺。
核心参数
- root:null 表示顶层视口;内层滚动列表应指定真正的滚动容器。
- rootMargin:扩大或缩小观察区域,可提前加载,但预加载距离要结合网络和资源大小。
- threshold:可见比例达到的阈值数组;阈值多会带来更多回调。
- entries:可能一次包含多个目标且回调异步批量触发,处理时使用 target/业务 ID,不依赖回调顺序。
- disconnect/unobserve:路由卸载、列表项移除或一次性任务完成后释放观察关系。
一次性懒加载和曝光
图片区预留宽高避免 CLS,靠近视口时再发起加载;成功后可 unobserve,失败则显示重试入口。曝光统计应同时判断交叉比例、页面 visibilityState、停留时间和业务去重窗口,避免瞬间掠过、后台标签页或反复滚动重复上报。
const seen = new Set();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const id = entry.target.getAttribute('data-id');
if (!id || seen.has(id)) continue;
seen.add(id);
reportExposure(id);
observer.unobserve(entry.target);
}
}, { root: null, rootMargin: '200px 0px', threshold: 0.01 });
document.querySelectorAll('[data-lazy]').forEach((node) => observer.observe(node));
示例中的 Set 仅在当前页面生效。跨页面/服务端去重应使用业务事件 ID 和合理时窗,不要把用户浏览轨迹永久写入 localStorage。
目录高亮与无限加载
阅读目录可观察标题,但多个标题同时交叉时需要确定优先级,例如选择靠近阅读锚点且处于方向上的标题。滚动容器、顶部粘性导航、字体变化和折叠内容都要覆盖;程序滚动到锚点时不要让高亮反向抖动。
无限列表用单个底部 sentinel;请求在途、没有下一页、错误和重复触发都要显式处理。成功追加后移动或复用 sentinel,并按游标去重。对键盘与读屏用户提供“加载更多”按钮作为替代,避免必须滚到最底才能访问新结果。
虚拟列表可能复用同一个 DOM 节点显示不同数据,观察回调要读取当下业务 ID 并清理旧状态。大型页面使用共享 observer 按配置分组,避免每个条目都创建一套观察器;但不可为少量目标引入过度抽象。
Observer 配置取舍
预加载用 rootMargin,曝光判定用 threshold 与停留时间,两者不要混成同一条件。rootMargin 只改变交叉区域,不代表元素真的被用户看见;交叠比例也无法识别被其他元素遮挡。对于广告/视频等可见性口径更严格的场景,应对照 IntersectionObserver 的能力限制并补充 Visibility API 或服务端业务规则。
一个 observer 的回调要尽量轻:只读 entry、更新小状态或投递轻量任务,避免同步读取大量布局信息和为每个目标重建列表。按相同 root/threshold 复用观察实例,设置合理的目标数量上限;大型虚拟列表优先由虚拟化组件管理挂载窗口。
测试和边界
测试不同 root、嵌套滚动、快速滚动跳过目标、目标在视口内插入、图片加载失败、路由切换、DOM 复用、网络延迟、后台标签页和小屏。用浏览器性能面板确认回调没有触发高成本同步布局;对于无法支持 IntersectionObserver 的目标环境提供 feature detection 和简单降级。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。