用 IntersectionObserver 替代 scroll 事件实现图片懒加载,避免主线程卡顿
图片懒加载是博客、电商列表、后台表格里最常见的性能优化手段。过去大多数实现依赖监听 `scroll` 事件,在回调里遍历所有图片,用 `getBoundingClientRect()` 判断是否进入视口。这种做法在页面图片数量少的时候没问题,可一旦列表有几百个节点,滚动时每次事件触发都同步计算布局,主线程会被大量 `getBoundingClientRect` 和样式读取占满,滚动掉帧、白屏、卡顿就跟着来了。更麻烦的是,`scroll` 事件本身触发频率极高,即使用节流,也无法避免在快速滚动时漏掉图片进入视口的那一帧。浏览器后来提供了 `IntersectionObserver`,它把“元素是否进入视口”的检测交给合成器线程异步处理,回调只在交叉状态变化时执行,主线程不再需要参与滚动计算。这篇文章会从实际代码出发,讲清楚如何用 `IntersectionObserver` 写一个可复用的懒加载指令,处理图片加载失败、占位图、提前加载距离,以及和现代框架结合时的注意事项。 ## 为什么 scroll 方案在长列表里会卡 先看一段典型的旧实现: ```js const images = document.querySelectorAll('img[data-src]') function lazyLoad() { let count = 0 images.forEach(img => { if (img.dataset.src && isInViewport(img)) { img.src = img.dataset.src img.removeAttribute('data-src') count++ } }) if (count === images.length) { window.removeEventListener('scroll', throttled) } } function isInViewport(el) { const rect = el.getBoundingClientRect() return rect.top 0 } const throttled = throttle(lazyLoad, 200) window.addEventListener('scroll', throttled) window.addEventListener('resize', throttled) ``` 这段代码的问题不在逻辑,而在执行时机。`scroll` 事件在滚动过程中可能每帧触发多次,节流到 200ms 只是减少了回调次数,但每次回调依然要在主线程完成所有图片的布局读取。当图片节点有 300 个,且它们分散在文档流里,`getBoundingClientRect()` 会强制浏览器重新计算布局,这就是典型的布局抖动。如果此时页面还有 CSS 动画或者正在加载字体,卡顿会非常明显。 `IntersectionObserver` 的模型不一样:你告诉它要观察哪些元素、视口作为根、提前多少距离触发,它自己会在合适的时机调用回调。回调里的 `entry.isIntersecting` 直接告诉你交叉状态,不需要再读布局。 ## 用 IntersectionObserver 写一个基础懒加载 先不封装,直接看核心逻辑: ```js const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return const img = entry.target const src = img.dataset.src if (!src) return img.src = src img.removeAttribute('data-src') observer.unobserve(img) }) }, { root: null, rootMargin: '200px 0px', threshold: 0.01, } ) document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img)) ``` `root: null` 表示以视口为根。`rootMargin: '200px 0px'` 让观察区域向外扩展 200px,图片还没进入视口就开始加载,用户滚动到的时候已经加载好了。`threshold: 0.01` 表示只要有 1% 可见就触发,避免大图完全进入才加载。加载完成后 `unobserve` 掉,避免重复触发。 这段代码已经能跑,但实际项目里图片会动态插入,比如分页加载、无限滚动、Vue/React 列表更新。所以需要把 observer 实例管理起来,并提供一个可复用的加载函数。 ## 封装成可复用模块 下面这个模块把 observer 单例化,支持动态注册和销毁: ```ts // lazy-image.ts type LazyOptions = { rootMargin?: string threshold?: number onLoad?: (img: HTMLImageElement) => void onError?: (img: HTMLImageElement) => void } let observer: IntersectionObserver | null = null let options: LazyOptions = {} function createObserver() { return new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return const img = entry.target as HTMLImageElement loadImage(img) observer?.unobserve(img) }) }, { root: null, rootMargin: options.rootMargin ?? '200px 0px', threshold: options.threshold ?? 0.01, } ) } function loadImage(img: HTMLImageElement) { const src = img.dataset.src if (!src) return const temp = new Image() temp.onload = () => { img.src = src img.removeAttribute('data-src') options.onLoad?.(img) } temp.onerror = () => { options.onError?.(img) } temp.src = src } export function initLazyImage(opts: LazyOptions = {}) { options = opts if (!observer) { observer = createObserver() } } export function observeImage(img: HTMLImageElement) { if (!observer) initLazyImage() observer!.observe(img) } export function unobserveImage(img: HTMLImageElement) { observer?.unobserve(img) } export function destroyLazyImage() { observer?.disconnect() observer = null } ``` 这里用 `new Image()` 预加载,等图片真正下载成功再替换 `src`,可以避免加载过程中出现裂图。如果图片加载失败,交给 `onError` 处理,比如换成占位图。 ## 在 Vue 和 React 里怎么接 在 Vue 3 里可以写成自定义指令: ```ts // lazy.ts import type { Directive } from 'vue' import { observeImage, unobserveImage } from './lazy-image' export const vLazy: Directive = { mounted(el, binding) { el.dataset.src = binding.value observeImage(el) }, updated(el, binding) { if (binding.value !== binding.oldValue) { unobserveImage(el) el.dataset.src = binding.value observeImage(el) } }, unmounted(el) { unobserveImage(el) }, } ``` 模板里直接写: ```html ``` React 里更适合封装成组件: ```tsx import { useEffect, useRef } from 'react' import { observeImage, unobserveImage } from './lazy-image' type LazyImageProps = { src: string alt?: string className?: string } export function LazyImage({ src, alt = '', className }: LazyImageProps) { const ref = useRef(null) useEffect(() => { const img = ref.current if (!img) return img.dataset.src = src observeImage(img) return () => unobserveImage(img) }, [src]) return } ``` React 的 effect 依赖 `src`,当图片地址变化时会先取消观察,再重新注册,不会出现旧地址还在观察的情况。 ## 占位图和加载失败的细节 懒加载不能只考虑“什么时候加载”,还要考虑“没加载时显示什么”。常见做法是给 `img` 一个低分辨率占位图或者纯色背景: ```css .lazy-img { display: block; width: 100%; height: auto; background: #f0f0f0; object-fit: cover; } ``` 如果后端支持,可以让 `data-src` 指向原图,`src` 直接给一个极小的 base64 模糊图。这样即使 JS 还没执行,用户也能看到模糊预览,不会有空白。 加载失败时,`onError` 里可以替换成兜底图: ```ts initLazyImage({ onError(img) { img.src = img.removeAttribute('data-src') }, }) ``` 注意失败后也要移除 `data-src`,否则下次滚动事件或者重新观察时还会尝试加载。 ## 和原生 loading="lazy" 的关系 现在浏览器原生支持 ``,它也是基于类似的异步观察机制,代码量更少: ```html ``` 但原生懒加载的触发距离由浏览器控制,不能自定义 `rootMargin`,也无法在加载完成后执行回调。对于需要精细控制提前量、需要占位图切换、需要统计曝光或者做骨架屏的场景,`IntersectionObserver` 仍然更合适。实际项目里可以两者结合:普通图片用 `loading="lazy"`,关键首屏图用 `IntersectionObserver` 控制。 ## 一个容易忽略的坑:observer 实例不要重复创建 如果每个组件都 `new IntersectionObserver`,页面里会存在大量 observer 实例,每个实例都有自己的回调队列。虽然浏览器对 observer 数量没有硬性上限,但实例太多会增加内存和调度开销。上面的模块用单例 `observer` 管理所有图片,通过 `observe` 和 `unobserve` 控制生命周期,是更稳妥的做法。 另外,在 SPA 路由切换时记得调用 `destroyLazyImage()`,否则旧的 observer 可能还在观察已经移除的 DOM 节点。虽然现代浏览器对已脱离文档的元素会降低观察优先级,但手动断开更干净。 ## 兼容性和降级 `IntersectionObserver` 在现代浏览器里支持很好,IE 完全不支持。如果项目还需要兼容 IE,可以动态检测: ```ts if (!('IntersectionObserver' in window)) { // 降级:直接加载所有图片 document.querySelectorAll('img[data-src]').forEach((img) => { const src = img.dataset.src if (src) img.src = src }) } ``` 降级方案不是再去写 scroll 监听,而是在不支持的环境里直接加载全部图片,保证功能可用。毕竟 IE 用户的设备性能本身也有限,再叠加 scroll 计算反而更容易卡死。 把图片加载从 scroll 事件里解放出来之后,滚动性能会有明显改善,尤其在中低端安卓机上。代码量没有增加多少,但主线程不再被无意义的布局读取占用,列表滚动和动画都能保持顺畅。 推荐查看天气:https://www.40tianqi.com/ningyang/7tian.html 推荐查看天气:https://www.40tianqi.com/ningyang/10tian.html 推荐查看天气:https://www.40tianqi.com/ningyang/15tian.html 推荐查看天气:https://www.40tianqi.com/ningyang/40tian.html 推荐查看天气:https://www.40tianqi.com/dongping/ 推荐查看天气:https://www.40tianqi.com/dongping/zuotian.html 推荐查看天气:https://www.40tianqi.com/dongping/7tian.html 推荐查看天气:https://www.40tianqi.com/dongping/10tian.html 推荐查看天气:https://www.40tianqi.com/dongping/15tian.html 推荐查看天气:https://www.40tianqi.com/dongping/40tian.html 推荐查看天气:https://www.40tianqi.com/xintai/ 推荐查看天气:https://www.40tianqi.com/xintai/zuotian.html 推荐查看天气:https://www.40tianqi.com/xintai/7tian.html 推荐查看天气:https://www.40tianqi.com/xintai/10tian.html 推荐查看天气:https://www.40tianqi.com/xintai/15tian.html 推荐查看天气:https://www.40tianqi.com/xintai/40tian.html 推荐查看天气:https://www.40tianqi.com/weihai/ 推荐查看天气:https://www.40tianqi.com/weihai/zuotian.html 推荐查看天气:https://www.40tianqi.com/weihai/7tian.html 推荐查看天气:https://www.40tianqi.com/weihai/10tian.html 推荐查看天气:https://www.40tianqi.com/weihai/15tian.html 推荐查看天气:https://www.40tianqi.com/weihai/40tian.html 推荐查看天气:https://www.40tianqi.com/huancui/ 推荐查看天气:https://www.40tianqi.com/huancui/zuotian.html 推荐查看天气:https://www.40tianqi.com/huancui/7tian.html 推荐查看天气:https://www.40tianqi.com/huancui/10tian.html 推荐查看天气:https://www.40tianqi.com/huancui/15tian.html 推荐查看天气:https://www.40tianqi.com/huancui/40tian.html 推荐查看天气:https://www.40tianqi.com/wendeng/ 推荐查看天气:https://www.40tianqi.com/wendeng/zuotian.html 推荐查看天气:https://www.40tianqi.com/wendeng/7tian.html 推荐查看天气:https://www.40tianqi.com/wendeng/10tian.html 推荐查看天气:https://www.40tianqi.com/wendeng/15tian.html 推荐查看天气:https://www.40tianqi.com/wendeng/40tian.html 推荐查看天气:https://www.40tianqi.com/weihairongcheng/ 推荐查看天气:https://www.40tianqi.com/weihairongcheng/zuotian.html 推荐查看天气:https://www.40tianqi.com/weihairongcheng/7tian.html 推荐查看天气:https://www.40tianqi.com/weihairongcheng/10tian.html 推荐查看天气:https://www.40tianqi.com/weihairongcheng/15tian.html 推荐查看天气:https://www.40tianqi.com/weihairongcheng/40tian.html 推荐查看天气:https://www.40tianqi.com/rushan/ 推荐查看天气:https://www.40tianqi.com/rushan/zuotian.html 推荐查看天气:https://www.40tianqi.com/rushan/7tian.html 推荐查看天气:https://www.40tianqi.com/rushan/10tian.html 推荐查看天气:https://www.40tianqi.com/rushan/15tian.html 推荐查看天气:https://www.40tianqi.com/rushan/40tian.html 推荐查看天气:https://www.40tianqi.com/rizhao/ 推荐查看天气:https://www.40tianqi.com/rizhao/zuotian.html 推荐查看天气:https://www.40tianqi.com/rizhao/7tian.html 推荐查看天气:https://www.40tianqi.com/rizhao/10tian.html 推荐查看天气:https://www.40tianqi.com/rizhao/15tian.html 推荐查看天气:https://www.40tianqi.com/rizhao/40tian.html 推荐查看天气:https://www.40tianqi.com/donggang/ 推荐查看天气:https://www.40tianqi.com/donggang/zuotian.html 推荐查看天气:https://www.40tianqi.com/donggang/7tian.html 推荐查看天气:https://www.40tianqi.com/donggang/10tian.html 推荐查看天气:https://www.40tianqi.com/donggang/15tian.html 推荐查看天气:https://www.40tianqi.com/donggang/40tian.html 推荐查看天气:https://www.40tianqi.com/wulian/ 推荐查看天气:https://www.40tianqi.com/wulian/zuotian.html 推荐查看天气:https://www.40tianqi.com/wulian/7tian.html 推荐查看天气:https://www.40tianqi.com/wulian/10tian.html 推荐查看天气:https://www.40tianqi.com/wulian/15tian.html 推荐查看天气:https://www.40tianqi.com/wulian/40tian.html 推荐查看天气:https://www.40tianqi.com/juxian/ 推荐查看天气:https://www.40tianqi.com/juxian/zuotian.html 推荐查看天气:https://www.40tianqi.com/juxian/7tian.html 推荐查看天气:https://www.40tianqi.com/juxian/10tian.html 推荐查看天气:https://www.40tianqi.com/juxian/15tian.html 推荐查看天气:https://www.40tianqi.com/juxian/40tian.html 推荐查看天气:https://www.40tianqi.com/laiwu/ 推荐查看天气:https://www.40tianqi.com/laiwu/zuotian.html 推荐查看天气:https://www.40tianqi.com/laiwu/7tian.html 推荐查看天气:https://www.40tianqi.com/laiwu/10tian.html 推荐查看天气:https://www.40tianqi.com/laiwu/15tian.html 推荐查看天气:https://www.40tianqi.com/laiwu/40tian.html 推荐查看天气:https://www.40tianqi.com/laicheng/ 推荐查看天气:https://www.40tianqi.com/laicheng/zuotian.html 推荐查看天气:https://www.40tianqi.com/laicheng/7tian.html 推荐查看天气:https://www.40tianqi.com/laicheng/10tian.html 推荐查看天气:https://www.40tianqi.com/laicheng/15tian.html 推荐查看天气:https://www.40tianqi.com/laicheng/40tian.html 推荐查看天气:https://www.40tianqi.com/gangcheng/ 推荐查看天气:https://www.40tianqi.com/gangcheng/zuotian.html 推荐查看天气:https://www.40tianqi.com/gangcheng/7tian.html 推荐查看天气:https://www.40tianqi.com/gangcheng/10tian.html 推荐查看天气:https://www.40tianqi.com/gangcheng/15tian.html 推荐查看天气:https://www.40tianqi.com/gangcheng/40tian.html 推荐查看天气:https://www.40tianqi.com/linyi/ 推荐查看天气:https://www.40tianqi.com/linyi/zuotian.html 推荐查看天气:https://www.40tianqi.com/linyi/7tian.html 推荐查看天气:https://www.40tianqi.com/linyi/10tian.html 推荐查看天气:https://www.40tianqi.com/linyi/15tian.html 推荐查看天气:https://www.40tianqi.com/linyi/40tian.html 推荐查看天气:https://www.40tianqi.com/luozhuang/ 推荐查看天气:https://www.40tianqi.com/luozhuang/zuotian.html 推荐查看天气:https://www.40tianqi.com/luozhuang/7tian.html 推荐查看天气:https://www.40tianqi.com/luozhuang/10tian.html 推荐查看天气:https://www.40tianqi.com/luozhuang/15tian.html 推荐查看天气:https://www.40tianqi.com/luozhuang/40tian.html 推荐查看天气:https://www.40tianqi.com/hedong/ 推荐查看天气:https://www.40tianqi.com/hedong/zuotian.html 推荐查看天气:https://www.40tianqi.com/hedong/7tian.html 推荐查看天气:https://www.40tianqi.com/hedong/10tian.html 推荐查看天气:https://www.40tianqi.com/hedong/15tian.html 推荐查看天气:https://www.40tianqi.com/hedong/40tian.html 推荐查看天气:https://www.40tianqi.com/yinan/ 推荐查看天气:https://www.40tianqi.com/yinan/zuotian.html 推荐查看天气:https://www.40tianqi.com/yinan/7tian.html 推荐查看天气:https://www.40tianqi.com/yinan/10tian.html 推荐查看天气:https://www.40tianqi.com/yinan/15tian.html 推荐查看天气:https://www.40tianqi.com/yinan/40tian.html 推荐查看天气:https://www.40tianqi.com/tancheng/ 推荐查看天气:https://www.40tianqi.com/tancheng/zuotian.html 推荐查看天气:https://www.40tianqi.com/tancheng/7tian.html 推荐查看天气:https://www.40tianqi.com/tancheng/10tian.html 推荐查看天气:https://www.40tianqi.com/tancheng/15tian.html 推荐查看天气:https://www.40tianqi.com/tancheng/40tian.html 推荐查看天气:https://www.40tianqi.com/yishui/ 推荐查看天气:https://www.40tianqi.com/yishui/zuotian.html 推荐查看天气:https://www.40tianqi.com/yishui/7tian.html 推荐查看天气:https://www.40tianqi.com/yishui/10tian.html 推荐查看天气:https://www.40tianqi.com/yishui/15tian.html 推荐查看天气:https://www.40tianqi.com/yishui/40tian.html 推荐查看天气:https://www.40tianqi.com/cangshan/ 推荐查看天气:https://www.40tianqi.com/cangshan/zuotian.html 推荐查看天气:https://www.40tianqi.com/cangshan/7tian.html 推荐查看天气:https://www.40tianqi.com/cangshan/10tian.html 推荐查看天气:https://www.40tianqi.com/cangshan/15tian.html 推荐查看天气:https://www.40tianqi.com/cangshan/40tian.html 推荐查看天气:https://www.40tianqi.com/feixian/ 推荐查看天气:https://www.40tianqi.com/feixian/zuotian.html 推荐查看天气:https://www.40tianqi.com/feixian/7tian.html 推荐查看天气:https://www.40tianqi.com/feixian/10tian.html 推荐查看天气:https://www.40tianqi.com/feixian/15tian.html 推荐查看天气:https://www.40tianqi.com/feixian/40tian.html 推荐查看天气:https://www.40tianqi.com/pingyi/ 推荐查看天气:https://www.40tianqi.com/pingyi/zuotian.html 推荐查看天气:https://www.40tianqi.com/pingyi/7tian.html 推荐查看天气:https://www.40tianqi.com/pingyi/10tian.html 推荐查看天气:https://www.40tianqi.com/pingyi/15tian.html 推荐查看天气:https://www.40tianqi.com/pingyi/40tian.html 推荐查看天气:https://www.40tianqi.com/junan/ 推荐查看天气:https://www.40tianqi.com/junan/zuotian.html 推荐查看天气:https://www.40tianqi.com/junan/7tian.html 推荐查看天气:https://www.40tianqi.com/junan/10tian.html 推荐查看天气:https://www.40tianqi.com/junan/15tian.html 推荐查看天气:https://www.40tianqi.com/junan/40tian.html 推荐查看天气:https://www.40tianqi.com/mengyin/ 推荐查看天气:https://www.40tianqi.com/mengyin/zuotian.html 推荐查看天气:https://www.40tianqi.com/mengyin/7tian.html 推荐查看天气:https://www.40tianqi.com/mengyin/10tian.html 推荐查看天气:https://www.40tianqi.com/mengyin/15tian.html 推荐查看天气:https://www.40tianqi.com/mengyin/40tian.html 推荐查看天气:https://www.40tianqi.com/linshu/ 推荐查看天气:https://www.40tianqi.com/linshu/zuotian.html 推荐查看天气:https://www.40tianqi.com/linshu/7tian.html 推荐查看天气:https://www.40tianqi.com/linshu/10tian.html 推荐查看天气:https://www.40tianqi.com/linshu/15tian.html 推荐查看天气:https://www.40tianqi.com/linshu/40tian.html 推荐查看天气:https://www.40tianqi.com/dezhou/ 推荐查看天气:https://www.40tianqi.com/dezhou/zuotian.html 推荐查看天气:https://www.40tianqi.com/dezhou/7tian.html 推荐查看天气:https://www.40tianqi.com/dezhou/10tian.html 推荐查看天气:https://www.40tianqi.com/dezhou/15tian.html 推荐查看天气:https://www.40tianqi.com/dezhou/40tian.html 推荐查看天气:https://www.40tianqi.com/decheng/ 推荐查看天气:https://www.40tianqi.com/decheng/zuotian.html 推荐查看天气:https://www.40tianqi.com/decheng/7tian.html 推荐查看天气:https://www.40tianqi.com/decheng/10tian.html 推荐查看天气:https://www.40tianqi.com/decheng/15tian.html 推荐查看天气:https://www.40tianqi.com/decheng/40tian.html 推荐查看天气:https://www.40tianqi.com/lingxian/ 推荐查看天气:https://www.40tianqi.com/lingxian/zuotian.html 推荐查看天气:https://www.40tianqi.com/lingxian/7tian.html 推荐查看天气:https://www.40tianqi.com/lingxian/10tian.html 推荐查看天气:https://www.40tianqi.com/lingxian/15tian.html 推荐查看天气:https://www.40tianqi.com/lingxian/40tian.html 推荐查看天气:https://www.40tianqi.com/ningjin/ 推荐查看天气:https://www.40tianqi.com/ningjin/zuotian.html 推荐查看天气:https://www.40tianqi.com/ningjin/7tian.html 推荐查看天气:https://www.40tianqi.com/ningjin/10tian.html 推荐查看天气:https://www.40tianqi.com/ningjin/15tian.html 推荐查看天气:https://www.40tianqi.com/ningjin/40tian.html 推荐查看天气:https://www.40tianqi.com/qingyun/ 推荐查看天气:https://www.40tianqi.com/qingyun/zuotian.html 推荐查看天气:https://www.40tianqi.com/qingyun/7tian.html 推荐查看天气:https://www.40tianqi.com/qingyun/10tian.html 推荐查看天气:https://www.40tianqi.com/qingyun/15tian.html 推荐查看天气:https://www.40tianqi.com/qingyun/40tian.html 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/ 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/zuotian.html 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/7tian.html 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/10tian.html 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/15tian.html 推荐查看天气:https://www.40tianqi.com/dezhoulinyi/40tian.html 推荐查看天气:https://www.40tianqi.com/qihe/ 推荐查看天气:https://www.40tianqi.com/qihe/zuotian.html 推荐查看天气:https://www.40tianqi.com/qihe/7tian.html 推荐查看天气:https://www.40tianqi.com/qihe/10tian.html