真正的感知式预取是提前触发资源加载与解码,而非等待元素进入视口;通过 rootMargin 控制提前量、WeakMap+Map 去重防并发、滚动趋势动态调参、decode 完成后再赋值 src,实现“一露头就能秒显”。

直接用 IntersectionObserver 做延迟加载器,不等于“感知式预取”。真正的感知式,是让系统在用户还没看到元素时,就预判它即将出现,并提前把资源拉到内存、解码完成、甚至准备好渲染状态——不是等它“进入视口”,而是让它“一露头就能秒显”。
核心:用 rootMargin 实现时间维度的提前触发
threshold=0 只能告诉你“刚露出 1px”,但网络延迟、图片解码、CDN 缓存未命中会让这 1px 变成卡顿起点。rootMargin 才是控制“提前量”的关键参数:
- 设为 "300px":相当于把视口向下扩展 300px,元素离真实视口还有 300px 时就触发预取
- 桌面端常用 "300px",中速滚动手机建议 "150px"
- 三折叠设备(如 Mate XT 的 G 态)需动态适配:
`${window.innerHeight * 0.3}px`,避免因屏幕尺寸突变导致预取失效
防重复与资源竞争:WeakMap + 全局 URL 去重
一个图片可能被多个模块 observe,或同一张图出现在不同位置。不加控制会引发并发请求激增、带宽浪费、CDN 限流。
- 用 WeakMap 缓存每个 DOM 元素的预取状态:
const prefetchState = new WeakMap(),key 是元素,value 是{ url, promise, timestamp } - 预取前先查缓存:promise 存在且未 reject,直接返回;已 resolve,直接赋值
img.src - 对相同
dataset.src做全局去重:用 Map 存url → Promise,避免 fetch 多次 - 限制并发数:内部用
Promise.allSettled(queue.slice(0, 3))控制最多同时预取 3 个资源
让预取真正“可感知”:结合滚动趋势动态调参
静态 rootMargin 不够智能。用户向下快速滚动时,应加大提前量;向上回滚时,则应收窄,减少误触发。
- 节流监听
scroll事件(仅用于趋势判断,不参与加载逻辑) - 采样最近 300ms 的
deltaY符号和幅度 - 若连续两次
deltaY > 0(向下滚动),rootMargin 动态增至 "400px" - 若向上滚动,收窄为 "100px"
- 对轮播图、放大镜等交互密集模块,监听
mouseenter,立即触发其下所有data-pre资源预取
确保资源可用:解码完成再赋值 src
预取 ≠ 预加载。fetch 到二进制数据后,图片还需解码才能渲染。直接赋值 src 可能导致主线程阻塞或闪白。
- 对
类型资源,fetch 后调用img.decode() - decode 成功后再设置
img.src,并移除data-src - 失败时保留原占位,不中断后续流程











