别再用防抖做懒加载了:120 张图只加载出 4 张

起因是我给图库页换懒加载方案,随手搜了下教程。结果每篇都信誓旦旦说自己的写法最好,还互相瞧不上。我也不懂这有什么好吵的,索性把能搜到的 9 种写法全部跑了一遍。

先说结果,免得你浪费五分钟:有一种写法,120 张图滚到底只加载出来 4 张,剩下 116 张从头到尾就没发起过请求。 更气人的是,这写法在教程里出镜率还不低。

我是怎么较这个劲的

120 张图,每张 67KB,本地服务,禁缓存。每张图从「进入视口」到「加载完成」的延迟逐张记录,服务端同时记请求时间戳。

滚法统一:每帧滚 24 像素,一口气滚到底,模拟你刷长列表「不看了,划走」的速度。每种写法跑三轮,三轮数据差不到 1 毫秒,稳得我一度怀疑自己忘了改代码。

九种写法全列出来:不懒加载、原生 loading="lazy"、原生加 fetchpriority=low、IntersectionObserver 三档预载(0px / 200px / 1000px)、裸 scroll 监听、scroll+rAF 合帧、scroll+防抖 250ms。

写法 首屏请求 提前就绪 图片就位延迟中位
不懒加载 120 提前 10 秒 0
原生 lazy 7 119 张提前 851ms 15ms
原生+low 优先级 7 提前 852ms 15ms
IO 0px 2 0 15ms
IO 200px 3 提前 119ms ---
IO 1000px 6 提前 685ms ---
scroll 裸监听 2 0 32ms
scroll+rAF 2 0 14ms
scroll+防抖 250ms 1 --- 615ms

防抖懒加载,白屏神器

看最后一行。615 毫秒中位延迟还不是最惨的,最惨的是那 4 张。

防抖的机制是每次触发就重置定时器。用户只要还在滚,scroll 事件就一个接一个来,那个 250ms 定时器被无限期推倒重来,加载检查一次都没执行。等用户滚到底停下来了,检查终于跑了------但视口里只剩最后那几张图。

对,防抖。就是搜索框「停止输入 300ms 再请求」那个防抖。防抖本身没错,错的是把它贴在滚动事件上。这相当于把外卖小哥防抖了:你越催,他越不动,等你饿死了他才拎着饭出门。

对照组 scroll+rAF 合帧,就位延迟 14ms。44 倍差距,同一台机器,同一批图片,唯一区别就是那个 250ms 的定时器。

要是你代码库里现在还有 scroll + debounce 的懒加载,别修了,直接删。这玩意唯一的优点是省流量------图片压根不加载,能不省吗。

手写党输给了浏览器自带的一行

不写防抖的手写方案,成绩都正常,问题在另一件事上。

原生 loading="lazy" 的预载距离,Chrome 官方口径约 1250px。实测数据也对得上:原生写法的图片平均提前 851ms 就绪。

而手写 IntersectionObserver 的教程,十个里有九个教你写 rootMargin: 200px。实测这配置只提前 119ms。想追平原生,得把预载拉到 1000px,实测提前 685ms。

翻译一下:你吭哧吭哧写二十行 JS 维护一个 IntersectionObserver,用户反而比浏览器自带的一行属性多等半秒。手写党委屈吗?委屈。但数据不撒谎。

顺手辟两个谣

「fetchpriority=low 能优化懒加载」------我加了跑一轮,数据跟没加逐项一样。这属性是给首屏大图抢带宽用的,不是给懒加载列的。别惦记了。

「裸 scroll 监听会卡」 ------120 张图这个规模下,九种写法帧率中位全部 17ms,长任务全部 0 次。裸监听全量扫 getBoundingClientRect 都不卡,只是就位延迟比 IO 慢一倍。真要吐槽它,槽点是慢,不是卡。

所以懒加载到底优化了什么?看首屏那列:不懒加载的页面打开瞬间抢光 120 张图,懒加载系只拿 1 到 7 张。它省的是流量,是你服务器的带宽账单,不是帧率。

有人会说

「headless Chrome 的数据能算数吗?」帧率那条在真机上未必复现,这个我认,不争。但防抖翻车是机制问题,预载距离是浏览器行为,这两条跟跑在哪台机器上没关系。

「防抖是你故意拿来凑数的吧?」我倒是想凑数,可惜它翻得最狠。而且这个场景一点也不罕见:长列表快刷、锚点跳转、按一下 End 键,全是一口气滚到底。防抖懒加载慢速浏览时能凑合,一快就死,测试用例稍微一慢它就隐身------这才是它阴的地方。

正确写法,直接抄

html 复制代码
<!-- 首屏图:eager + 高优先级,LCP 别懒加载 -->
<img src="hero.jpg" loading="eager" fetchpriority="high" width="800" height="600">

<!-- 其余全部:一行原生,预载 1250px,比多数手写都激进 -->
<img src="a.jpg" loading="lazy" width="300" height="225">

非要手写 IO 的话(要配骨架屏、淡入之类的),把 rootMargin 至少给到 200px,想跟原生打平就 1000px:

js 复制代码
const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (e.isIntersecting) {
      e.target.src = e.target.dataset.src;
      io.unobserve(e.target);
    }
  }
}, { rootMargin: '1000px 0px' });
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));

最后统计一下:你们项目里现在还有 scroll+debounce 的懒加载吗?评论区举个手,我看看有多少人跟我一样是从这个坑里爬出来的。


相关推荐
计算机魔术师1 小时前
Hugging Face CEO 转评前 DOGE 研究人员用 AI 智能体渗透 Anthropic 系统事件
前端
IT_陈寒1 小时前
刚解决一个 Java 并发问题,这坑踩得我怀疑人生
前端·人工智能·后端
前端一课1 小时前
一个晚上,四个“小问题”:我重写了 12 章 ArkUI 笔记
前端
小码哥_常1 小时前
前端图片压缩实战——我把网站图片体积砍了 70%
前端
张一凡932 小时前
把“不要乱改”从提示词里拿出来,放进可校验的提案里
前端·后端·架构
一只爱撸猫的程序猿2 小时前
当任何人都能生成一万行代码:什么变得稀缺
人工智能·后端·程序员
木子欢儿2 小时前
Debian 13 + vsftpd 搭建FTP服务
linux·服务器·前端·网络·debian
leeguoo2 小时前
让 Claude Code / Codex 用上你已登录的 Chrome
前端·chrome·ai编程
LeoLi6382 小时前
随机应变,不是见招拆招:AI 干 6 倍工时之后,我把它做成了三层降级链路
人工智能·程序员