今天写了一个图片比较多的页面,差点没被加载速度搞疯。十几张图一上来就全往浏览器里塞,结果页面半天刷不出来,滚动的时候还一卡一卡的,控制台里全是图片加载的请求,看得我头都大了。查了一下这种情况得用 “懒加载”,试了一下果然舒服多了,今天就把我折腾出来的这点心得好好聊聊。
其实刚开始我特纳闷,图片多就多呗,浏览器不是能自己加载吗?后来才明白,浏览器加载图片的时候,每一张图都是一个 HTTP 请求
,而浏览器对同一域名的并发请求是有限制的(一般就 6 个左右)。这就好比超市结账,就 6 个收银台,你一下子推来几十车东西,后面的只能排队等着。图片也是一样,一下子全加载,后面的图片就得等前面的加载完才能开始,整个页面的加载时间就被拖得老长。更要命的是,用户可能就看个首屏,下面的图片根本不会滚动到,这些白加载的图片纯属浪费带宽和资源,服务器压力也大。
懒加载说白了就是 “按需加载”—— 只有当图片快要滚到用户能看到的地方时,再去加载它的真实地址。就像书店摆书,不用把所有书都摊在地上,读者走到哪个书架前,再把那一层的书摆出来就行。
那具体怎么实现呢?我看了不少例子,核心思路其实挺简单的。首先,图片的真实地址不能直接放在 src 里,因为 src 一被浏览器读到,就会立刻发请求。所以得找个地方暂存真实地址,HTML5 的 data-*
属性就很合适,比如用 data-original 存真实图片的 URL。那 src 放什么呢?可以放一张很小的占位图,比如加载中的动图(就像我代码里用的 loading.gif),这样既不会占太多带宽,又能告诉用户 “这里有图片,正在准备中”。
下面是具体的代码实现,我把关键部分都加了注释:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载</title>
<style>
.image-item {
width: 100%;
height: auto;
display: block;
margin-bottom: 10px;
}
</style>
</head>
<body>
<!-- 注意这里的图片结构:src放占位图,data-original放真实地址,lazyload属性标记这是个需要懒加载的图片 -->
<img class="image-item" lazyload="true" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif" data-original="https://img.36krcdn.com/hsossms/20250313/v2_15ad8ef9eca34830b4a2e081bbc7f57a@000000_oswg172644oswg1536oswg722_img_000?x-oss-process=image/resize,m_mfit,w_960,h_400,limit_0/crop,w_960,h_400,g_center" data-loaded="false"/>
<img class="image-item" lazyload="true" src="https://static.360buyimg.com/item/main/1.0.12/css/i/loading.gif" data-original="https://img.36krcdn.com/hsossms/20250312/v2_aeaa7a1d51e74c3a8f909c96cd73a687@000000_oswg169950oswg1440oswg600_img_jpeg?x-oss-process=image/format,webp" data-loaded="false"/>
<script>
const viewHeight = document.documentElement.clientHeight;
const eles = document.querySelectorAll('img[data-original][lazyload]');
// 懒加载的核心函数
const lazyload = function () {
Array.prototype.forEach.call(eles, function(item, index) {
if(item.dataset.original === "" || data-loaded==="true") return;
const rect = item.getBoundingClientRect();
if(rect.bottom >= 0 && rect.top <= viewHeight) {
const img = new Image();
img.src = item.dataset.original;
img.onload = function() {
item.src = item.dataset.original;
// 避免重复处理
item.dataset.loaded = true;
}
}
});
}
window.addEventListener('scroll', lazyload);
document.addEventListener('DOMContentLoaded', lazyload);
</script>
</body>
</html>
接下来就是判断什么时候加载真实图片。关键在于搞清楚 “图片有没有进入视口”。这里我用到了 getBoundingClientRect ()
这个方法,它能返回元素相对于浏览器视口的位置信息,里面的 top
和 bottom
属性特别有用。top 是元素顶部到视口顶部的距离,bottom 是元素底部到视口顶部的距离。如果 top 小于视口高度(说明元素顶部没超出视口底部),而且 bottom 大于 0(说明元素底部没超出视口顶部),那就说明这张图正在视口里,或者马上要滚进来了,这时候就该加载它了。
判断完位置,就得加载图片了。直接把 data-original
的值赋给 src 行不行?理论上可以,但有个小问题:如果图片很大或者网速慢,图片加载过程中可能会显示破碎的图标。所以更好的办法是先用 new Image ()
创建一个临时图片对象,把真实地址赋给这个临时图片的 src,等它加载完成(onload 事件触发),再把真实地址赋给页面上的 img 元素。这样用户看到的就是完整加载好的图片,体验会好很多。
加载完之后还有个细节:记得把 data-original 和 lazyload 这两个属性删掉。因为我们后面还会监听滚动事件,反复检查图片状态,删掉这些属性,下次就不会再处理这张图了,能省点性能。
然后就是事件监听
了。光写个判断方法还不够,得让它在合适的时机执行。首先,页面刚加载完
的时候,得检查一次首屏的图片,不然用户打开页面,首屏的图还得等滚动了才加载,那就很奇怪了。这时候可以用 DOMContentLoaded
事件,页面结构一加载完就执行一次懒加载检查。
另外,用户滚动页面的时候,新的图片会进入视口,所以还得监听 scroll 事件,每次滚动都触发检查。不过这里有个坑:scroll 事件触发得太频繁了,用户稍微一动就可能触发几十次,很耗性能。我后来查了资料,知道可以用 “节流”(throttle)来优化,比如每隔 100 毫秒才执行一次检查,既能保证图片及时加载,又不会太占用资源。
说到优化,后来我又发现了一个更优雅的解决方案 —— 用 IntersectionObserver(交叉观察器)。这东西简直是为懒加载量身定做的,原理特别有意思:它就像一个 “自动导购员”,你告诉它要盯着哪些图片,它会自己默默观察这些图片有没有进入视口,一旦检测到 “图片和视口交叉了”(也就是用户快看到了),就主动通知你处理,完全不用手动监听 scroll 事件。
具体实现起来也不复杂,代码大概是这样:
function addObserver(){
// 找出所有需要被观察的图片
const eles = document.querySelectorAll('img[data-original][lazyload]');
// 创建一个观察器实例,定义观察到交叉时的处理逻辑
const observer = new IntersectionObserver(function(changes){
changes.forEach(change=>{
// 当图片进入视口(交叉比例在0到1之间)或者写成 change.isIntersecting
if(change.intersectionRatio > 0 && change.intersectionRatio <=1){
const img = new Image();
img.src = change.target.dataset.original;
img.onload = function(){
change.target.src = img.src;
change.target.removeAttribute('data-original');
change.target.removeAttribute('lazyload');
observer.unobserve(change.target);
}
}
})
})
eles.forEach(ele=>{
observer.observe(ele);
})
}
addObserver();
用 IntersectionObserver 有几个明显的好处:首先,完全不用监听 scroll 事件,也就不需要手动做节流了,省了不少代码;其次,它是异步执行的,不会阻塞主线程,就算图片再多,也不会让页面卡顿;最后,它内部处理了元素位置计算,避免了 getBoundingClientRect () 带来的回流问题,性能好得多。
说到这里,其实浏览器现在也支持原生的懒加载了,就是给 img 标签加个 loading="lazy"
属性,简单得很。但为啥还要自己写呢?主要是兼容性问题,一些老浏览器(比如 IE)不支持这个属性,自己实现的话能兼容更多场景。而且亲手写一遍,能更明白里面的原理,以后遇到问题也知道怎么调。
总的来说,懒加载不算啥高深技术,但确实是优化页面性能的好办法。作为新手,能把这些小细节搞明白,看着页面从卡顿到流畅,还是挺有成就感的。如果你们也遇到图片太多加载慢的问题,不妨试试这个方法,有啥问题咱们也可以一起讨论讨论。