注册
web

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

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

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




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


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


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


<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 用户,我们只能回到石器时代。


// 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 做统一调度。


// 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 KB0.3 KB(含 polyfill 2 KB)

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




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


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


// 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);

模板里直接写:


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


作者:前端微白
来源:juejin.cn/post/7530854092869615635

0 个评论

要回复文章请先登录注册