核心是复用IntersectionObserver单例、提前加载、加载后立即unobserve并销毁时disconnect,避免监听器与闭包内存泄漏。

核心思路是复用事件监听器、限制监听范围、及时清理,而不是为每个图片单独绑定 scroll 或 intersectionObserver。
用 IntersectionObserver 替代 scroll + getBoundingClientRect
scroll 事件高频触发,配合 getBoundingClientRect 计算开销大,且容易因频繁绑定/解绑监听器导致引用残留。IntersectionObserver 是浏览器原生的轻量级方案,自动管理目标元素的可见性检测,不依赖手动绑定和计算。
- 一个 Observer 实例可监控成百上千张图片,无需为每张图 addEventListener
- 回调中处理完加载后,可调用 unobserve(el) 主动移除对该图片的监听(避免重复触发)
- 创建时传入 { rootMargin: '100px' } 提前加载,体验更顺滑
确保 Observer 实例全局唯一且可复用
不要在组件初始化或循环中反复 new IntersectionObserver。应封装成单例或在模块顶层创建一次,供所有懒加载图片共享。
- 多个组件共用同一个 observer,避免内存中堆积多个监听器实例
- 若需不同配置(如不同 root 或阈值),最多按配置维度缓存几个 observer,而非按图片数量创建
- 页面卸载或组件销毁时,调用 observer.disconnect() 彻底释放资源
图片加载完成即解除监听,避免冗余保留
一旦图片 onload 成功或加载失败(onerror),必须立刻 unobserve,否则该 DOM 节点仍被 observer 持有,阻碍 GC。
- 在 load/error 回调里执行 observer.unobserve(imgEl)
- 如果使用 Promise 封装加载逻辑,确保 reject/resolve 后也 unobserve(例如网络超时、404)
- 避免“只监听不清理”的模式——比如监听 scroll 后忘记 removeEventListener,或 observer 未 unobserve 已加载图片
避免闭包意外捕获大对象
如果自定义回调函数内引用了组件实例、大型数据或 DOM 树,可能造成内存无法释放。
- 回调中只访问必要字段(如 img.src、img.dataset.src),不直接传入 this 或整个 state
- 使用箭头函数需谨慎:若它捕获了外层作用域的大对象,observer 会间接持有它们
- 必要时用弱引用结构(如 WeakMap 缓存映射关系),但通常 unobserve + 简洁回调已足够
不复杂但容易忽略:真正撑爆内存的往往不是图片本身,而是堆积的监听器+闭包+未清理的 DOM 引用。用好 IntersectionObserver 并严格 unobserve,基本就解决了 90% 的问题。










