一张 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 图,世界和平。

相关推荐
ObjectX前端实验室1 小时前
【react18原理探究实践】scheduler原理之Task 完整生命周期解析
前端·react.js
ObjectX前端实验室1 小时前
【react18原理探究实践】调度器(Scheduler)原理深度解析
前端·react.js
路漫漫心远1 小时前
音视频学习笔记十八——图像处理之OpenCV检测
前端
摸着石头过河的石头1 小时前
从零开始玩转前端:一站式掌握Web开发基础知识
前端·javascript
sniper_fandc2 小时前
关于Mybatis-Plus的insertOrUpdate()方法使用时的问题与解决—数值精度转化问题
java·前端·数据库·mybatisplus·主键id
10岁的博客2 小时前
技术博客SEO优化全攻略
前端
南屿im3 小时前
别再被引用坑了!JavaScript 深浅拷贝全攻略
前端·javascript
tracyZhang3 小时前
Android低版本bitmap native分配实现原理
android·性能优化
想要一辆洒水车3 小时前
vuex4源码分析学习
前端
sophie旭3 小时前
一道面试题,开始性能优化之旅(6)-- 异步任务和性能
前端·javascript·性能优化