一张 8K 海报差点把首屏拖垮

你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。

老板一句「能不能像朋友圈那样滑到哪看到哪?」------于是你把懒加载重新翻出来折腾了一轮。


解决方案:三条技术路线,你全踩了一遍

1. 最偷懒:原生 loading="lazy"

一行代码就能跑,浏览器帮你搞定。

html 复制代码
<img
  src="https://cdn.xxx.com/poster1.jpg"
  loading="lazy"
  decoding="async"
  width="800" height="450"
/>

🔍 关键决策点

  • loading="lazy" 2020 年后现代浏览器全覆盖,IE 全军覆没。
  • 必须写死 width/height,否则 CLS 会抖成 PPT。

适用场景 :内部系统、用户浏览器可控,且图片域名已开启 Accept-Ranges: bytes(支持分段加载)。


2. 最稳妥:scroll 节流 + getBoundingClientRect

老项目里还有 5% 的 IE11 用户,我们只能回到石器时代。

js 复制代码
// utils/lazyLoad.js
const lazyImgs = [...document.querySelectorAll('[data-src]')];
let ticking = false;

const loadIfNeeded = () => {
  if (ticking) return;
  ticking = true;
  requestAnimationFrame(() => {
    lazyImgs.forEach((img, idx) => {
      const { top } = img.getBoundingClientRect();
      if (top < window.innerHeight + 200) { // 提前 200px 预加载
        img.src = img.dataset.src;
        lazyImgs.splice(idx, 1); // 🔍 及时清理,防止重复计算
      }
    });
    ticking = false;
  });
};

window.addEventListener('scroll', loadIfNeeded, { passive: true });

🔍 关键决策点

  • requestAnimationFrame 把 30 ms 的节流降到 16 ms,肉眼不再掉帧。
  • 预加载阈值 200 px,实测 4G 网络滑动不白屏。

缺点:滚动密集时 CPU 占用仍高,列表越长越卡。


3. 最优雅:IntersectionObserver 精准观测

新项目直接上 Vue3 + TypeScript,我们用 IntersectionObserver 做统一调度。

ts 复制代码
// composables/useLazyLoad.ts
export const useLazyLoad = (selector = '.lazy') => {
  onMounted(() => {
    const imgs = document.querySelectorAll<HTMLImageElement>(selector);
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            const img = e.target as HTMLImageElement;
            img.src = img.dataset.src!;
            img.classList.add('fade-in'); // 🔍 加过渡动画
            io.unobserve(img);            // 观测完即销毁
          }
        });
      },
      { rootMargin: '100px', threshold: 0.01 } // 🔍 提前 100px 触发
    );
    imgs.forEach((img) => io.observe(img));
  });
};
  1. 浏览器合成线程把「目标元素与视口交叉状态」异步推送到主线程。
  2. 主线程回调里只做一件事:把 data-src 搬到 src,然后 unobserve
  3. 整个滚动期间,零事件监听,CPU 占用 < 1%。

原理剖析:从「事件驱动」到「观测驱动」

维度 scroll + 节流 IntersectionObserver
触发时机 高频事件(~30 ms) 浏览器内部合成帧后回调
计算量 每帧遍历 N 个元素 仅通知交叉元素
线程占用 主线程 合成线程 → 主线程
兼容性 IE9+ Edge79+(可 polyfill)
代码体积 0.5 KB 0.3 KB(含 polyfill 2 KB)

一句话总结:把「我每隔 16 ms 问一次」变成「浏览器你告诉我啥时候到」。


应用扩展:把懒加载做成通用指令

在 Vue3 项目里,我们干脆封装成 v-lazy 指令,任何元素都能用。

ts 复制代码
// directives/lazy.ts
const lazyDirective = {
  mounted(el: HTMLImageElement, binding) {
    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          el.src = binding.value; // 🔍 binding.value 就是 data-src
          io.disconnect();
        }
      },
      { rootMargin: '50px 0px' }
    );
    io.observe(el);
  },
};

app.directive('lazy', lazyDirective);

模板里直接写:

html 复制代码
<img v-lazy="item.url" :alt="item.title" />

举一反三:三个变体场景思路

  1. 无限滚动列表

    IntersectionObserver 绑在「加载更多」占位节点上,触底即请求下一页,再把新节点继续 observe,形成递归观测链。

  2. 广告曝光统计

    广告位 50% 像素可见且持续 1 s 才算一次曝光。设置 threshold: 0.5 并在回调里用 setTimeout 延迟 1 s 上报,离开视口时 clearTimeout

  3. 背景图懒加载

    背景图没有 src,可以把真实地址塞在 style="--bg: url(...)",交叉时把 background-image 设成 var(--bg),同样零回流。


小结

  • 浏览器新特性能救命的,就别再卷节流函数了。
  • 写死尺寸、加过渡、及时 unobserve,是懒加载不翻车的三件套。
  • 把观测器做成指令/组合式函数,后续业务直接零成本接入。

现在你的「企业风采」首屏降到 1.2 s,老板滑得开心,运营继续传 8K 图,世界和平。

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼5 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端