什么是懒加载?
懒加载(Lazy Loading)是一种网页性能优化技术,它通过延迟加载当前视口外的非关键资源(如图片、视频等),直到用户滚动页面、资源即将进入可视区域时才加载。这项技术能显著减少页面初始加载时间,节省带宽,并提升用户体验。
在电商网站中,这种效果尤为常见:当用户向下滚动页面时,会先看到占位图或加载动画,稍作停顿后,真实的商品图片才会逐渐加载显示。这种渐进式的加载方式既保证了页面的流畅性,又避免了不必要的资源消耗。
本文一些内容是参考一位掘友的:前端性能优化之图片懒加载「三种原生实现+vue指令」
懒加载有什么好处??
- 减少初始加载时间:懒加载通过延迟加载非首屏内容(如图片、视频),让核心资源优先加载,大幅缩短页面可交互时间,提升用户的第一印象。
- 节省带宽:只加载用户实际浏览到的资源,避免浪费流量请求未触达的图片或脚本,尤其对流量敏感的用户(如移动端)至关重要。
- 降低服务器负载:减少一次性并发请求数量,避免服务器在高峰时段因过多冗余请求而响应变慢,提高整体稳定性。
懒加载的实现原理
懒加载的核心原理是 延迟加载非关键资源,直到它们即将进入用户可视区域时才加载。
关键实现步骤:
- 占位替代先用轻量占位(如空白图、低分辨率图)代替真实资源。当然现在有些网页为了节省,还会用一些灰色的背景图片来代替。简单来说就是一开始就将图片的
src属性设为空字符串或者是一些低分辨率图片的地址。当然因为用的是同一张图片,这样就只会加载一次图片,因为其他的都是在缓存中得到的。 - 动态加载:当元素进入视口时,替换占位内容为真实资源(如将
data-src赋给src)。当然判断元素进入窗口就有很多种方法了,也是我们今天要讲的主要内容。
适用场景:
📷 图片、🖼️ 视频、📜 长列表、📦 非关键脚本/样式等。
一句话总结:懒加载就是「看不见不加载,快看见时才加载」。
懒加载的代码实现
我们先来看看html代码,不使用懒加载是什么样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载</title>
<style>
* {
margin: 0;
padding: 0;
}
img {
width: 500px;
height: 500px;
margin-bottom: 20px;
display: block;
}
</style>
</head>
<body>
<img src="../img/1.webp" alt="">
<img src="../img/2.webp" alt="">
<img src="../img/3.webp" alt="">
<img src="../img/4.webp" alt="">
<img src="../img/5.webp" alt="">
<img src="../img/6.webp" alt="">
<img src="../img/7.webp" alt="">
</body>
</html>
下面就是没有使用懒加载的图片效果,虽然有点模糊,但是可以清楚的看到,我们刷新页面或者已进入这个网页,它就把所有的图片加载出来了。
一些在最下面的图片我们还没看到但是也一下子直接加载了,这样对于一些图片有上百上千张的网站来说,负担就很大,给用户的体验造成不好的效果。
那么下面就让我们一起走入懒加载的世界看看有多少美妙的API实现这个功能吧🚀🚀
方法一:
我们可以使用原生的loading="lazy"属性来实现懒加载(只有现代浏览器可以支持),让我们来看看代码吧!!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原生属性实现懒加载</title>
<style>
* {
margin: 0;
padding: 0;
}
img {
width: 500px;
height: 500px;
margin-bottom: 20px;
display: block;
}
</style>
</head>
<body>
<img src="../img/1.webp" alt="">
<img src="../img/2.webp" alt="">
<img src="../img/3.webp" alt="" loading="lazy">
<img src="../img/4.webp" alt="" loading="lazy">
<img src="../img/5.webp" alt="" loading="lazy">
<img src="../img/6.webp" alt="" loading="lazy">
<img src="../img/7.webp" alt="" loading="lazy">
</body>
</html>
下面就是效果图了,我们可以看到最终还是实现了懒加载的功能。
作用原理
- 默认行为:浏览器会立即加载所有图片/iframe(无论是否在可视区域)。
- 使用
loading="lazy":
浏览器只加载当前视口(Viewport)内的资源,当用户滚动到资源附近时(通常距离视口边缘一定阈值),才触发加载。
优点我就不详谈了,跟懒加载一样的,我们来说说缺点吧🚀🚀
缺点
-
兼容性限制:旧浏览器(如 IE、Safari 14 之前)不支持,需用
JavaScript Polyfill补救(如Intersection Observer API)。 -
滚动体验问题:若用户快速滚动,可能短暂看到空白或未加载的内容(可通过占位符缓解)。
-
布局偏移风险(CLS):未设置图片尺寸时,加载后可能导致页面内容突然下移。
解决方案:始终指定width和height属性。 -
不适用于关键资源:首屏图片/Logo 使用懒加载会延迟显示,应避免。
-
影响统计工具:部分分析工具可能无法准确统计未加载的图片曝光量。
浏览器限制
方法二:
方法二可以参看下面的图片进行理解,如果 图片顶部到文档顶部的距离 < 浏览器可视窗口高度 + 滚动条滚过的高度 那么该图片就应该出现在可视区域内了。
但是还有一个问题,就是一些页面他会提供一键到底的按钮,又或者用户直接滑到页面底部,那么这个判断条件对所有的图片都为真,还是会造成性能问题。所以我们要再加上一条判断条件 图片的高度 + 图片顶部到文档顶部的距离 > 滚动条滚过的高度,以确保图片确实在可视区域内,而不只是被滑过。
图片中涉及到了好几个API,不了解的小伙伴可以看:Element:scrollTop、Window.innerHeight、HTMLElement.offsetTop等相关文档
好了,既然知道了判断方法,就让我们来实现它吧🚀🚀在此声明之后用的所有html文档都是下面的,只会改动不同的script文件,来实现懒加载功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载</title>
<style>
* {
margin: 0;
padding: 0;
}
img {
width: 500px;
height: 500px;
margin-bottom: 20px;
display: block;
}
</style>
</head>
<body>
<!-- 下面一开始加载的都是占位图片,而data-src是真正的图片地址,当图片进入可视区域时,会将data-src的值赋给src,从而加载真正的图片。 -->
<img data-src="../img/1.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/2.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/3.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/4.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/5.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/6.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<img data-src="../img/7.webp" alt="" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif">
<script src="./lazyload1.js"></script>
</body>
</html>
可以看到在上面的html文件中所有的img标签里面的src都是同一张占位图,这样做可以加快页面的渲染速度,然后通过js交互将data-src里面的地址给src,从而构建真实的页面效果。这种"占位-监听-替换"的三步机制,既确保了页面快速呈现,又实现了图片的精准按需加载。
下面就是lazyload1.js的文件:
/**
* 懒加载函数:动态查询并加载进入可视区域的图片
*/
function lazyLoad() {
console.log("触发懒加载..."); // 调试信息,表示函数被调用
// 动态查询所有仍带有 data-src 属性的图片(确保每次处理的是未加载的图片)
const imgs = document.querySelectorAll('img[data-src]');
// 获取当前窗口的可视高度和滚动高度
const windowHeight = window.innerHeight;
const scrollHeight = document.documentElement.scrollTop;
// 遍历所有待加载的图片
imgs.forEach(img => {
const imgHeight = img.offsetHeight; // 图片高度
const imgTop = img.offsetTop; // 图片顶部距离文档顶部的偏移量
// 检查图片是否进入可视区域:
// 1. 窗口底部(windowHeight + scrollHeight)是否超过图片顶部(imgTop)
// 2. 图片底部(imgHeight + imgTop)是否超过窗口顶部(scrollHeight)
if (windowHeight + scrollHeight > imgTop && imgHeight + imgTop > scrollHeight) {
// 将 data-src 的值赋给 src,触发图片加载
img.src = img.dataset.src;
// 移除 data-src 属性,标记为已加载
img.removeAttribute('data-src');
}
});
}
// 初始加载:立即检查首屏图片
lazyLoad();
// 监听滚动事件,触发懒加载
window.addEventListener('scroll', lazyLoad);
到这里我们就实现了一个简单的懒加载的功能了,让我们一起来看看效果:
ok呀,我们可以看到图片都是我们滚动到相应的位置才会加载出来,如果没用到就不会进行加载了,大大缓解了浏览器的压力。但是这是最好的写法么??当然不是,我们可以看到我写了console.log("触发懒加载..."),那么让我们看看它在页面上打印了多少次吧
咳咳,可能看的不是很清楚呀,有需要的小伙伴可以自己去试验一下,简简单单滑动几下就触发了几百次的滚动事件,大量的滚动事件的触发对浏览器的性能来说是一个比较大的负担。
那么我们就可以通过防抖和节流来实现了,没接触过的小伙伴可以尝试看看💡 防抖与节流:前端高手的必备技能,告别卡顿!这篇文章。防抖和节流都可以规避频繁的触发回调函数,那么这两个我们选哪个比较好呢??听大佬说选择节流比较好,它是一定时间就会触发一次。而防抖它是等事件执行完成之后再调用回调函数,这样的话,可能用户一直来回滚动,就会导致图片一直加载不出来,从而影响用户的体验。那么我们就用节流来优化懒加载吧!!
下面就是添加了计时器的节流函数的js代码了
/**
* 节流函数:在指定的时间间隔内只执行一次回调函数
* @param {Function} fn 需要节流执行的回调函数
* @param {number} delay 节流的时间间隔(毫秒)
* @param {...any} args 回调函数 fn 的固定参数(可选)
* @returns {Function} 返回一个节流后的函数
*/
function throttle(fn, delay, ...args) {
let timer = null; // 存储定时器ID
// 返回节流处理后的函数
return function(...params) {
// 合并固定参数和动态参数(如事件对象)
const allArgs = [...args, ...params];
// 保存当前执行上下文(this指向)
const context = this;
// 如果定时器不存在,表示可以执行新调用
if (!timer) {
// 设置定时器,延迟执行
timer = setTimeout(() => {
// 使用apply调用原函数,确保正确的this和参数
fn.apply(context, allArgs);
// 执行后重置定时器标识
timer = null;
}, delay);
}
// 如果定时器存在,忽略本次调用
};
}
// 使用示例:监听滚动事件,加载后面的图片
// throttle 返回一个节流处理后的函数
// lazyLoad: 实际要执行的图片加载函数
// 500: 节流时间间隔(ms)
// imgs: 固定参数(图片元素集合)
window.onscroll = throttle(lazyLoad, 500, imgs);
那为什么要用这种闭包timer定时器来实现节流,而不是使用时间戳的方式实现呢?原因如下所示:
行为特性差异
| 特性 | 定时器方案 | 时间戳方案 |
|---|---|---|
| 首次触发 | 延迟执行 | 立即执行 |
| 停止触发后 | 保证执行最后一次回调 ✅ | 停止即终止 ❌ |
| 执行节奏 | 固定间隔的尾部执行 | 固定间隔的头部执行 |
选择定时器实现的核心原因是:必须保证滚动停止后的最后一次检测。这对于懒加载至关重要——用户滚动到新位置停止时,往往正是需要加载新图片的关键时刻。时间戳方案无法满足此需求,而定时器方案通过"延迟执行+尾部触发"的特性完美解决这个问题,同时避免初始执行的冗余计算,是性能与功能完整性的最佳平衡。
说了这么多让我们来看看最终的效果吧!!
这样就触发了几次懒加载,大大减少了对浏览器的负担,并且我们可以清楚地看到之前的占位图的图片。
方法三:
方法三其实就是换了一个API而已,但是本质上跟上面那种方法差不多,连节流和性能优化也差不多,那么让我们走进新的API吧!!
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
DOMRect对象,是包含整个元素的最小矩形(包括 padding 和 border-width)。该对象使用 left、top、right、bottom、x、y、width 和 height 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。可以参考下图:
我们需要判断data-src存在并没有被删除,表明图片还没有被渲染,已经被渲染的图片就不再重新渲染了;还有元素底部相对于可视窗口顶部的距离(bottom) >= 0 来确保图片还在可视窗口的下方而不是一闪而过;最后就是元素顶部相对于可视窗口顶部的距离(top) < 可视窗口的高度 来确保图片出现在可视窗口的区域。那么根据方法二的代码进行相关的修改融合就可以得到方法三了,js代码如下:
// 获取所有带有 data-src 属性的图片元素(初始页面中的图片)
const imgs = document.querySelectorAll('img[data-src]');
function lazyLoad() {
console.log("触发懒加载..."); // 调试信息,表示函数被调用
const windowHeight = window.innerHeight; // 获取当前窗口可视区域的高度
// 使用 Array.prototype.forEach.call 遍历类数组对象(imgs)
Array.prototype.forEach.call(imgs, img => {
// 跳过 data-src 为空的图片
if (img.dataset.src === '') return;
// 获取图片相对于视口的位置信息
const rect = img.getBoundingClientRect();
/**
* 检测图片是否进入视口:
* 1. rect.bottom >= 0: 图片底部在视口顶部下方(或刚好接触顶部)
* 2. rect.top < windowHeight: 图片顶部在视口底部上方
* 同时满足表示图片至少有一部分在可视区域内
*/
if (img.dataset.src && rect.bottom >= 0 && rect.top < windowHeight) {
// 将 data-src 的值赋给 src 属性,触发图片加载
img.src = img.dataset.src;
// 移除 data-src 属性避免重复加载
img.removeAttribute('data-src');
}
});
}
// 页面加载时立即执行一次懒加载
lazyLoad();
/**
* 节流函数:在指定的时间间隔内只执行一次回调函数
* @param {Function} fn 需要节流执行的回调函数
* @param {number} delay 节流的时间间隔(毫秒)
* @param {...any} args 回调函数 fn 的固定参数(可选)
* @returns {Function} 返回一个节流后的函数
*/
function throttle(fn, delay, ...args) {
let timer = null; // 存储定时器ID,用于控制执行频率
// 返回节流处理后的函数
return function (...params) {
// 合并固定参数和动态参数(如事件对象)
const allArgs = [...args, ...params];
// 保存当前执行上下文(this指向)
const context = this;
// 如果定时器不存在,表示可以执行新调用
if (!timer) {
// 设置定时器,延迟执行
timer = setTimeout(() => {
// 使用apply调用原函数,确保正确的this和参数
fn.apply(context, allArgs);
// 执行后重置定时器标识
timer = null;
}, delay);
}
// 如果定时器存在,忽略本次调用
};
}
// 使用节流函数包装懒加载功能:
// 1. 指定要执行的函数:lazyLoad
// 2. 设置节流间隔:500ms
// 3. 传入固定参数:imgs(初始图片集合)
window.onscroll = throttle(lazyLoad, 500, imgs);
那么相关的效果图还有一些优化啥的就不进行展示了。但是还是要看看浏览器的支持情况的:
方法四:
IntersectionObserver是一个现代浏览器提供的 JavaScript API,用于异步监听目标元素与其祖先元素或视口(viewport)的交叉状态变化。它高效解决了传统滚动检测的性能问题:通过回调函数在元素进入/离开可视区域或达到指定阈值时触发,开发者无需频繁计算元素位置,即可实现懒加载、无限滚动、曝光统计、动画触发等常见功能,显著提升页面性能与用户体验。
它也是我在这里面比较推荐使用的一个API。使用方法和一些内容的解释如下所示:
1. 创建观察器
const observer = new IntersectionObserver(callback, options);
callback:当被观察的元素进入/离开视口时,自动执行的回调函数。options(可选):配置观察行为(如触发条件、观察范围等)。
2. options 配置(可选)
{
root: null, // 观察的根元素,默认是浏览器视口(null 代表整个窗口)
rootMargin: "10px", // 扩大或缩小观察范围(如 "10px 20px" 表示上下 10px,左右 20px)
threshold: 0.5 // 触发回调的阈值(0.5 表示元素 50% 进入视口时触发)
}
-
root:- 默认
null(观察元素是否进入 整个浏览器窗口)。 - 如果指定某个 DOM 元素(如
root: document.querySelector("#scrollArea")),则观察元素是否进入 该容器的可视区域。
- 默认
-
rootMargin:- 类似 CSS
margin,可以提前或延后触发回调。 - 例如
"10px"表示 元素距离视口还有 10px 时就触发(适合预加载)。
- 类似 CSS
-
threshold:- 取值范围
0~1,表示 元素可见比例达到多少时触发回调。 0:刚进入视口(哪怕 1px)就触发。1:完全进入视口才触发。[0, 0.5, 1]:分别在 0%、50%、100% 时触发。
- 取值范围
3. 观察目标元素
const target = document.querySelector("#myElement");
observer.observe(target); // 开始观察这个元素
- 可以观察多个元素(多次调用
observe())。 - 停止观察某个元素:
observer.unobserve(target)。 - 停止所有观察:
observer.disconnect()。
4. 回调函数(callback)
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("元素进入视口!");
// 执行操作(如加载图片、播放动画)
} else {
console.log("元素离开视口!");
}
});
}
-
entries:数组,包含所有被观察元素的状态变化。 -
entry的关键属性:isIntersecting(布尔值):true表示元素进入视口,false表示离开。intersectionRatio(0~1):元素可见的比例(如0.3表示 30% 可见)。target:当前被观察的 DOM 元素。
OK呀,我们通过上面内容的学习,已经基本可以使用IntersectionObserver方法了,那么上代码吧🚀🚀
/**
* 添加 IntersectionObserver 观察器,实现图片懒加载功能
*/
function addObserver() {
// 获取所有带有 data-src 属性的 img 元素(待懒加载的图片)
const imgs = document.querySelectorAll('img[data-src]');
/**
* 创建 IntersectionObserver 实例
* @param {IntersectionObserverEntry[]} entries - 被观察元素的交叉状态数组
*/
const observer = new IntersectionObserver(entries => {
// 遍历所有发生交叉状态变化的元素
entries.forEach(entry => {
// 判断元素是否进入视口(isIntersecting 为 true 表示进入视口)
if (entry.isIntersecting) {
// 获取当前进入视口的图片元素
const img = entry.target;
// 将 data-src 的值赋给 src 属性,触发图片加载
img.src = img.dataset.src;
// 移除 data-src 属性(可选,防止重复加载)
img.removeAttribute('data-src');
// 图片已加载,停止观察该元素以优化性能
observer.unobserve(img);
}
});
}, {
// 可选的配置项(这里使用默认配置):
// root: null, // 默认为视口
// rootMargin: '0px', // 观察范围不扩展
// threshold: 0 // 元素刚进入视口就触发
});
// 遍历所有待观察的图片元素,开始观察每个元素
imgs.forEach(img => {
observer.observe(img);
});
}
// 执行函数,启动图片懒加载观察
addObserver();
效果图如下所示:
我们可以清楚的看到实现了懒加载的功能,而且不用添加防抖或者节流等函数,所以说IntersectionObserver函数对于实现懒加载是非常有帮助的,这也是我为什么推荐这个API的原因。当然这个由于这个API很多的mdn中都标注了实验性技术,所以要提前看看浏览器是否支持该API
ok呀,那就讲到这边了,当然还会有一些其他的写法,但是我并没有了解到,所以就先列出这几种方法。我个人是比较喜欢第一种和第四种的都挺方便,中间的一般就是面试可能会考察的,所以还是需要了解一下的。
总结
懒加载(Lazy Loading) 是一种优化网页性能的技术,通过延迟加载非关键资源(如图片、视频等),仅在它们进入或即将进入用户视口时才加载,从而减少初始页面加载时间、节省带宽并提升用户体验。实现原理通常基于 IntersectionObserver API 监听元素与视口的交叉状态,当目标元素(如带有 data-src 的图片)滚动到可视区域时,动态替换属性(如 data-src → src)触发加载,加载后停止观察以避免重复处理。适用于图片列表、长页面内容或广告等场景,显著降低首屏加载压力并减少无效资源请求。
上面呢就是笔者接触到的一些方法总结,如果你喜欢呢,可以一键三连以防文章找不到了😊😊当然如果碰到了小错误也可以在评论区指出来,看到了就会修改一下,欢迎大家提出宝贵的意见,再见啦🚀🚀🚀