IntersectionObserver 预取需用 rootMargin 提前触发、WeakMap 去重缓存、滚动趋势动态调参、三折叠设备适配尺寸突变,并通过 img.decode() 确保解码完成再赋值 src。

直接回答:不用等用户滚动到某处才加载,而是让 IntersectionObserver 提前“感知”元素即将进入视口,在它真正可见前 200–400ms 就触发资源预取 —— 这不是懒加载,是带预测能力的主动调度。
为什么 threshold=0 不够用?
默认 threshold: 0 意味着只要目标元素哪怕露出 1px,回调就触发。但实际中,图片解码、网络延迟、CDN缓存未命中会导致“看到即卡顿”。单纯靠 isIntersecting 判断已不可靠。
- 阈值设为
[0, 0.25, 0.5]只能告诉你“当前可见比例”,无法表达“即将进来”的时间维度 -
rootMargin是唯一能提前触发的可控参数:比如设rootMargin: "200px",相当于把视口向下扩展 200px,元素离视口还差 200px 时就被判定为“将交集” - 移动端滚动快,
rootMargin建议按设备 dpr 和常见滚动速度反推:桌面端常用"300px",中速滚动手机建议"150px",三折叠设备(如 Mate XT 的 G 态)需动态适配屏幕高度,用`${window.innerHeight * 0.3}px`
如何避免重复预取和资源竞争?
一个模块可能被多次 observe,或多个模块共用同一张图 URL;不加控制会导致并发请求激增、带宽浪费、甚至触发 CDN 限流。
- 用
WeakMap缓存每个target对应的预取状态:const prefetchState = new WeakMap();,key 是 DOM 元素,value 是{ url, promise, timestamp } - 预取前先查缓存:若
promise存在且未 reject,直接return promise;若已 resolve,直接赋值img.src - 对同一
dataset.src做全局去重:用Map存url → Promise,避免相同资源被 fetch 多次 - 限制并发数:内部用
Promise.allSettled([...queue].slice(0, 3))控制最多同时预取 3 个资源
怎么让预取真正“可感知”?
所谓“感知式”,核心是把预取时机和用户行为意图挂钩,而不是机械响应像素变化。这需要结合滚动趋势做简单预测。
- 监听
scroll事件节流采样(仅用于趋势判断,不用于加载逻辑),计算最近 300ms 的deltaY符号和幅度 - 若连续两次采样
deltaY > 0(向下滚动),则把rootMargin动态加大到"400px";反之向上滚动时收窄为"100px",减少误触发 - 对轮播图、放大镜等交互密集模块,监听其
mouseenter事件,立即触发该模块下所有data-prefetch图片的预取,无视 observer 状态 - 关键路径资源(如主图)仍走
preload,IntersectionObserver预取只用于次级资源(缩略图、色盘图、Lottie JSON)
三折叠设备上的特殊处理点
Mate XT 类设备在 F/M/G 态切换时,window.innerHeight 可能在 100ms 内突变 2–3 次,导致 rootMargin 计算失效、observer 错误重置。
- 不要在
resize中直接调用observer.disconnect()+observe(),会丢失 pending 状态 - 改用
ResizeObserver监听根容器(如#pdp-main)尺寸变化,仅当高度变化 > 10% 时才更新rootMargin - 对三屏态(G 态),禁用横向滚动预取:检测
screen.orientation.type === 'landscape-primary'且window.innerWidth > 2000时,关闭 X 轴方向的 margin 扩展 - 折叠动画期间(
transition过程中)暂停所有预取:监听document.body上的foldstatechange自定义事件(华为提供)
最易被忽略的是:预取 ≠ 加载完成。你拿到的是 fetch().then(res => res.arrayBuffer()),但图片解码、渲染管线插入仍在线程外异步发生。务必用 img.decode() 确保解码完成后再赋值 src,否则仍可能触发 layout shift。










