loading="lazy"仅对img和iframe原生生效,且无回调;IntersectionObserver需正确配置root、threshold(推荐[0.01])、rootMargin(带单位),并用isIntersecting&&intersectionRatio>0双重校验,加载后及时unobserve防泄漏。

为什么 loading="lazy" 在某些场景下根本不起作用
因为浏览器只对 <img> 和 <iframe> 原生支持 loading="lazy",且仅在支持该属性的浏览器中生效;对 <div>、自定义组件或 JS 驱动的内容(比如图表、列表项)完全无效。更关键的是:它不触发回调,无法与业务逻辑联动——你没法知道“这张图刚进入视区了”,也就没法执行初始化、埋点或动画。
用 IntersectionObserver 替代时必须设对的三个参数
直接 new 一个观察器却没配好选项,是懒加载逻辑失效最常见的原因。重点不是“有没有用”,而是“怎么用才稳定”:
-
root:不填默认是视口;若容器本身有滚动(比如弹窗内列表),必须显式传入该容器元素,否则判断基准错位 -
threshold:别硬写[0];想“一露头就触发”,用[0.01]更可靠(避免因像素四舍五入导致错过首次交点) -
rootMargin:预加载缓冲区,例如"100px"表示元素距离视口顶部还有 100px 时就开始加载;但注意单位必须带引号,且不能写成"100"(会静默失败)
示例:
const io = new IntersectionObserver(cb, { rootMargin: "150px", threshold: [0.01] });
监听多个元素时,entries 里藏着两个易忽略的状态细节
回调函数收到的 entries 数组中,每个 entry 的状态需要同时检查两项才能安全执行加载:
立即学习“前端免费学习笔记(深入)”;
-
entry.isIntersecting === true:表示当前处于交集状态(但可能是从 false 切换过来,也可能是持续 true) -
entry.intersectionRatio > 0:确保确实有像素重叠(某些安卓 WebView 中isIntersecting可能误报为 true,但 ratio 仍为 0)
所以稳妥判断应写成:
if (entry.isIntersecting && entry.intersectionRatio > 0) { /* 执行加载 */ }。漏掉后者,在部分低端机型上会出现“明明没看见却已加载”的问题。
加载完成后必须调用 unobserve(),否则内存泄漏比想象中快
尤其在 React/Vue 等框架中动态渲染列表时,如果每次 mount 都新建 observer 并监听元素,又不手动 unobserve,observer 实例会持续持有 DOM 引用,GC 无法回收。更隐蔽的问题是:同一个元素被多次 observe,会导致一次滚动触发多次回调。
正确做法是:在加载逻辑完成(比如图片 onload、数据请求返回并渲染完毕)后立刻执行 io.unobserve(entry.target)。如果需复用(如滚动回退后重新加载),改用 io.observe(el) 显式重绑,而不是反复 new。
复杂点在于:异步加载(如 fetch + 渲染)完成后,元素可能已被移除(比如列表页切换),此时调用 unobserve() 会抛 NotFoundError。建议包一层 try-catch,或先用 entry.target.isConnected 做存在性校验。



















