前言
在电商、社交等图片密集型应用中,一次性加载所有图片会导致首屏白屏时间(FP)过长,消耗用户大量流量。图片懒加载 的核心思想就是: "按需加载" ------只有当图片进入或即将进入可视区域时,才真正发起网络请求。
一、 核心原理
- 占位图 :初始化时,图片的
src属性指向一张极小的 base64 图片或 loading 占位图。 - 存储地址 :将真实的图片 URL 存放在自定义属性中(如
data-src)。 - 触发判断 :通过 JS 监听位置变化,当图片进入可视区,将
data-src的值赋给src。
二、 方案对比与实现
方案 1:传统滚动监听(Scroll + OffsetTop)
这是最基础的方案,通过计算绝对位置来判断。
公式: window.innerHeight (可视窗口高) + document.documentElement.scrollTop (滚动条高度) > element.offsetTop (元素距离页面顶部高度)
代码实现:
JavaScript
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
const clientHeight = window.innerHeight;
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
images.forEach(img => {
// 判断是否进入可视区
if (clientHeight + scrollTop > img.offsetTop) {
img.src = img.dataset.src;
img.removeAttribute('data-src'); // 加载后移除属性,防止重复执行
}
});
}
// 注意:必须添加节流(Throttle),防止滚动时高频触发导致卡顿
window.addEventListener('scroll', throttle(lazyLoad, 200));
方案 2:现代位置属性(getBoundingClientRect)
此方法通过getBoundingClientRect获取元素相对于浏览器视口的位置来判断,逻辑更简洁。
判断条件: rect.top(元素顶部距离视口顶部的距离) < window.innerHeight。
代码实现:
JavaScript
function handleScroll() {
const img = document.getElementById('target-img');
const rect = img.getBoundingClientRect();
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// 元素顶部出现在视口内,且没有超出视口底部
if (rect.top >= 0 && rect.top < viewHeight) {
console.log('图片进入可视区,开始加载');
img.src = img.dataset.src;
window.removeEventListener('scroll', handleScroll); // 加载后卸载监听
}
}
window.addEventListener('scroll', handleScroll);
方案 3:最优解方案(IntersectionObserver API)
IntersectionObserver这是目前最推荐的方案,它是异步的,不会阻塞主线程,且不需要手动计算位置,性能最高。
使用语法:const observer = new IntersectionObserver(callback, options)
callback:是元素可见性发生变化时的回调函数,接收两个参数:entries:观察目标的对象数组。对象中存在isIntersecting属性(布尔值),代表目标元素是否与根元素交叉(即进入视口)。
options:配置对象(该参数可选)。其中root表示指定一个根元素,默认是浏览器窗口、rootMargin表示控制根元素的外边距、threshold为目标元素与根元素中的可见比例,可以通过设置值来触发回调函数
代码实现:
JavaScript
const observerOptions = {
root: null, // 默认为浏览器视口
rootMargin: '0px 0px 50px 0px', // 提前 50px 触发加载,提升用户体验
threshold: 0.1 // 交叉比例达到 10% 时触发
};
const handleIntersection = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
console.log('IntersectionObserver 推送:图片加载成功');
observer.unobserve(img); // 停止观察该元素
}
});
};
const observer = new IntersectionObserver(handleIntersection, observerOptions);
// 观察页面中所有带 data-src 的图片
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
方案 1 和 2 在获取
offsetTop或getBoundingClientRect时会强制浏览器重新计算布局(回流),在长列表场景下可能导致掉帧。方案 3 不受此影响
三、总结
| 特性 | 方案 1 (OffsetTop) | 方案 2 (Rect) | 方案 3 (Intersection) |
|---|---|---|---|
| 计算复杂度 | 较高 (需计算累计高度) | 中等 | 极低 (引擎原生实现) |
| 性能消耗 | 高 (频繁触发回流) | 高 (触发回流) | 低 (异步非阻塞) |
| 兼容性 | 极好 (所有浏览器) | 好 (IE9+) | 一般 (现代浏览器, IE 需 Polyfill) |
四、补充
- 现代浏览器(Chrome 76+)已原生支持
<img loading="lazy">,如果是简单场景,一行 HTML 属性即可搞定。 - 务必给图片设置固定的宽高比或底色占位。否则图片加载前高度为 0,加载瞬间高度撑开会引发剧烈的页面抖动。