前言
你一定有这样的经历:打开一个图片超多的页面,进度条像蜗牛一样爬,屏幕上全是空白的占位框,你甚至怀疑自己的网是不是欠费了。
其实这不是你的网络问题,而是页面在一股脑加载所有图片,哪怕它们还在屏幕外面躺着。这就好比疯狂星期四你点了一份全家桶,结果服务员把一年的量都端了上来,你吃不完,还得花大力气搬回家。
好了,那么遇到问题就要解决问题。懒加载就是来解决这个问题的 “聪明服务员”—— 它只在你需要的时候,才把图片端到你面前。
除了图片,比如视频、音频、甚至是整个 DOM 模块(比如长列表里的卡片、分页内容)都可以用懒加载来优化,本文用图片做代表。
一、懒加载到底是什么?
简单来说,懒加载就是 “按需加载”。通俗来讲不就是 “懒得加载” 嘛。
具体实现如下:
- 页面刚打开时,只加载当前屏幕里能看到的图片(或视频等等)。
- 等你往下滚动页面,其他图片才会在进入视野的瞬间加载出来。
- 这样一来,页面首次加载速度直接起飞,用户体验瞬间拉满。
就比如刷小红书刷快了,图片就会变成这样:
这可不能让网络背锅,这就是运用到了 -- 懒加载,既不耗太多性能,也不影响用户的体验。
二、传统懒加载:用滚动监听 + 节流实现
这是最经典的懒加载方案,核心思路是监听滚动事件,判断图片是否进入了可视区域。
先上代码:第一个懒加载实现
// 可视区域高度
const visibleHeight = window.innerHeight;
// 懒加载函数
function lazyLoad() {
const imgs = document.querySelectorAll('img[lazyload]');
imgs.forEach(item => {
const rect = item.getBoundingClientRect();
// 判断图片是否在可视区域内
if (rect.top < visibleHeight && rect.bottom >= 0) {
item.src = item.dataset.origin;
item.removeAttribute('lazyload');
}
})
}
// 节流函数:防止滚动事件触发太频繁
function throttle(fn, await) {
let preTime = Date.now();
return () => {
const nowTime = Date.now();
if (nowTime - preTime >= await) {
preTime = nowTime;
fn();
}
}
}
// 初始化和监听
lazyLoad();
window.addEventListener('scroll', throttle(lazyLoad, 200));
原理拆解:玩转懒加载
window.innerHeight先拿到当前屏幕的高度,这是我们的 “视野范围”getBoundingClientRect()用来获取图片元素相对于视口的位置。- 当图片的顶部进入视口(
rect.top < visibleHeight),并且底部还没完全离开视口(rect.bottom >= 0)时,就把data-origin里的真实地址赋值给src,图片就开始加载了。 - 最后别忘了用节流函数
throttle,不然滚动一下触发几十次lazyLoad,性能开销会很大。
了解原理我们直接加上html代码,并且找10张图片实践一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
img{
display: block;
height: 200px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/OG4Cnt2SgXAuTj-Vv77ASGszUj1BwOhUXtBCplSlBfQmAAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/OceYG4p3E6SGFqFBN500JOHSiO4recmt6S1_fml1rlgUEAA/641" alt="">
<img src="" lazyload="true" data-origin="https://img1.baidu.com/it/u=2631091293,484496842&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="">
<img src="" lazyload="true" data-origin="https://img2.baidu.com/it/u=3107839948,3570219976&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O98tXzU2bQgRJ5nRiiJQx_6uwzkB_rsF9e6TA7kcXr058AA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Oz13eqQ6iXnUgh-eQxuXT8VmWvogpk66DvRM52Oe0QbNQAA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Otvjht3x4wA58ZkSIxSo88NZTsyD6ZdUyiLMHZmGFoHokAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/O7ZsQ9IrSfcWAWLPeaRcfeEt5FdyeTfnFYrSGmDSKlU0sAA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O_2D2lSAfCS8o9_fGMsuNxOArO_rS1j9Nc2vsYaEaq2PwAA/641" alt="">
<script>
// 可视区域的高度
const visibleHeight = window.innerHeight;
// 懒加载函数
function lazyLoad(){
const imgs = document.querySelectorAll('img[lazyload]');
// console.log(imgs);
// 判断哪些 img 在可视区域内
imgs.forEach(item => {
console.log(123);
const rect = item.getBoundingClientRect();
if(rect.top < visibleHeight && rect.bottom >= 0){
item.src = item.dataset.origin;
item.removeAttribute('lazyload');
}
})
}
lazyLoad();
// 监听滚动事件
window.addEventListener('scroll', throttle(lazyLoad, 200));
// 节流函数
function throttle(fn, await){ // 在规定时间内只执行一次
let preTime = Date.now();
return () => {
const nowTime = Date.now();
if (nowTime - preTime >= await){
preTime = nowTime;
fn();
}
}
}
</script>
</body>
</html>
可以看到我刚打开浏览器就出现了4张图片,并且这4张图片已经加载好了。
很好,那当我们往下滑时,你会发现图片只要在可视区域就加载出来:
这个大家可以自己copy代码去试试因为图片看不出效果😭。
三、现代懒加载:用 IntersectionObserver 躺赢
传统方案需要自己监听滚动、计算位置,有点像手动开车。而 IntersectionObserver 就像自动驾驶,它会自动告诉你元素什么时候进入了视口。
看看这个更优雅的实现:
const io = new IntersectionObserver(
() => {
console.log('hello');
},
{
threshold: [0, 0.25, 0.5, 0.75, 1.0]
}
)
io.observe(document.getElementById('box'));
首先要明确:threshold 配置的核心作用是指定元素可见比例的触发节点,你配置的 [0, 0.25, 0.5, 0.75, 1.0],意味着当 box 元素的可见度达到 0%、25%、50%、75%、100% 时,都会触发一次回调函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
height: 2000px;
}
#box{
height: 100px;
width: 100px;
background-color: brown;
margin: 100px;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
const io = new IntersectionObserver(
() => {
console.log('hello');
},
{
threshold: [0, 0.25, 0.5, 0.75, 1.0]
}
)
io.observe(document.getElementById('box'));
</script>
</body>
</html>
也就是说,我们定义一个棕色的小方块,当这个方块的可见度到0%、25%、50%、75%、100%时,就会打印一次hello。
- 最开始的时候,可见度为
100%所以直接打印一次hello:
- 当经过一半时,可见度分别经过了
25%、50%,算上最开始的共打印3次:
了解原理后我们一样用上面的10张图片来举例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载演示</title>
<style>
img{
display: block;
height: 200px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- 所有图片先不填真实src,用data-origin存真实地址,加lazyload标记 -->
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/OG4Cnt2SgXAuTj-Vv77ASGszUj1BwOhUXtBCplSlBfQmAAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/OceYG4p3E6SGFqFBN500JOHSiO4recmt6S1_fml1rlgUEAA/641" alt="">
<img src="" lazyload="true" data-origin="https://img1.baidu.com/it/u=2631091293,484496842&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="">
<img src="" lazyload="true" data-origin="https://img2.baidu.com/it/u=3107839948,3570219976&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O98tXzU2bQgRJ5nRiiJQx_6uwzkB_rsF9e6TA7kcXr058AA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Oz13eqQ6iXnUgh-eQxuXT8VmWvogpk66DvRM52Oe0QbNQAA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Otvjht3x4wA58ZkSIxSo88NZTsyD6ZdUyiLMHZmGFoHokAA/641" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/O7ZsQ9IrSfcWAWLPeaRcfeEt5FdyeTfnFYrSGmDSKlU0sAA/1000" alt="">
<img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O_2D2lSAfCS8o9_fGMsuNxOArO_rS1j9Nc2vsYaEaq2PwAA/641" alt="">
<script>
// 1. 实例化 IntersectionObserver,传入回调函数
const io = new IntersectionObserver(
(entries) => {
// entries 是被观察元素的状态数组
entries.forEach(item => {
// isIntersecting 为 true 表示元素进入了视口
if(item.isIntersecting){
// 把 data-origin 里的真实地址赋值给 src,触发图片加载
item.target.src = item.target.dataset.origin;
// 移除 lazyload 标记,避免重复处理
item.target.removeAttribute('lazyload');
// 加载完成后停止观察该元素,节省性能
io.unobserve(item.target);
}
})
}
)
// 2. 获取所有需要懒加载的图片,逐个进行观察
const imgs = document.querySelectorAll('img[lazyload]');
imgs.forEach(item => {
io.observe(item);
})
</script>
</body>
</html>
逻辑拆解:了解代码核心
-
HTML 部分:图片的
src留空(避免默认加载),用data-origin自定义属性存储真实的图片地址,同时添加lazyload="true"作为筛选标记,方便后续获取需要懒加载的图片。 -
实例化 IntersectionObserver:创建一个观察者实例,传入的回调函数会在被观察元素的可见状态变化时触发。
-
isIntersecting判断:这是最关键的属性,它直接告诉我们元素是否进入了视口,不用自己手动计算位置,省去了大量冗余代码。 -
加载图片并停止观察:当图片进入视口后,把
data-origin赋值给src,图片就会开始加载;加载完成后,用io.unobserve(item.target)停止观察该图片,避免后续滚动时重复触发回调,优化性能。 -
批量观察图片:通过
querySelectorAll获取所有带lazyload标记的图片,循环调用io.observe(item)让观察者 “盯紧” 这些图片。
进阶优化:配置 threshold 提前加载
如果你觉得 “进入视口才加载” 有点慢,想让图片提前一点点加载(比如元素露出 10% 就开始加载),可以添加 threshold 配置,让用户体验更流畅:
// 实例化时添加配置项
const io = new IntersectionObserver(
(entries) => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.origin;
item.target.removeAttribute('lazyload');
io.unobserve(item.target);
}
})
},
{
threshold: 0.1 // 元素可见度达到 10% 时触发回调
}
)
优势总结
- 不用监听
scroll事件,也不用手动计算元素位置,代码量少且易维护。 - 浏览器原生支持,内部做了性能优化,比手动节流的滚动监听更高效。
- 支持灵活配置,比如提前加载、指定观察根元素等,满足不同场景需求。
四、HTML 原生懒加载:一行代码搞定
如果你觉得 JavaScript 方案还是有点麻烦,那 HTML 原生的懒加载了解一下?
<img src="placeholder.jpg" loading="lazy" alt="图片">
只需要给img标签加个loading="lazy"属性,浏览器就会自动帮你实现懒加载。不过它的兼容性还不是 100%,所以目前项目中还是用JavaScript方案更稳妥。
五、总结
懒加载是解决 “图片多导致页面加载慢” 的实用技巧,核心是 “按需加载”—— 只在图片进入用户视野时才加载,既提升页面速度,又优化用户体验。
本文列举了3种方法:
1. 传统方案:靠scroll监听 +getBoundingClientRect()计算位置,搭配节流函数避免性能浪费,但代码繁琐、需手动处理逻辑。
2. 现代方案(推荐) :用IntersectionObserver“自动驾驶” 式监听元素可见状态,通过isIntersecting判断是否加载,代码简洁、性能更优,还能通过threshold配置提前加载。
3. 原生方案:HTML 的loading="lazy"属性一行代码搞定,但兼容性不足,暂不适合全场景。
结语
从手动计算位置的 “原始人” 方案,到 IntersectionObserver 的 “自动驾驶”,再到 HTML 原生的 “躺平” 方案,懒加载的进化史,就是前端开发者追求 “更懒、更高效” 的历史。无论你用哪种方案,核心思想都是一样的:不要让用户为他们还没看到的内容买单。
下次再遇到图片加载慢的问题,别再骂网络了,试试懒加载,让你的页面飞起来!