图片的懒加载意味着浏览器只在需要的时候才加载图片。在有很多图片的网页上,有效地使用懒加载可以显著地减少初始加载时间。但是背景图片呢?它们不是像标准的图片标签那样被加载的,所以我们经常会看到有大背景图片的网页在性能上有损失。有没有简单的方法来避免这个问题呢?
懒加载 - 以前和现在的对比
图片的懒加载在过去的多年里已经被许多不同的JS库实现了。通常的做法是:
- 不要设置图片标签的src值,而是把图片的url添加到data-src值里。
- 给每个图片标签加一个类来表示它是一个懒加载的候选者,比如class=“lazyload”。
- 附加一个JS库,它会绑定到滚动事件,并检查当一个img.lazyload出现在用户当前的滚动位置附近的时候。
- 当它出现的时候,把data-src的值换到src属性里,触发图片的加载。
- 移除lazyload类,这样这个元素就不会在用户继续滚动的时候被重新加载。
这种方法很受欢迎,但是在性能方面经常有很大的问题。绑定到滚动事件是非常影响性能的,Chrome现在会在每次看到这种情况的时候主动警告。
引入loading=lazy
loading
属性把这个工作从JS插件的手中拿走,让浏览器来优化它。支持度很广泛,意味着任何有这个属性设置为lazy
的图片都不会被加载,直到它接近视口。加载被触发的视口距离由浏览器来处理,并且包含了像用户的连接速度这样的信号:
On fast connections (4G), we reduced Chrome’s distance-from-viewport thresholds from 3000px to 1250px and on slower connections (3G or lower), changed the threshold from 4000px to 2500px.
在快速的连接(4G)上,我们把Chrome的视口距离阈值从3000px降低到1250px,而在慢速的连接(3G或更低)上,把阈值从4000px降低到2500px。
当正确地应用这个属性的时候,它可以对性能产生惊人的影响。通过推迟图片的加载,初始页面加载时间可以大幅度地减少。重要的注意事项是,这个属性不是应用在页面上的所有图片上 - 把视口上方的图片懒加载会损害用户体验,以及核心网络指标。
<img src="photo.jpg" height="450" width="600" alt="house photo"
loading="lazy"
/>
把这个技术应用到一个图片很多的网站上,我们就能看到在初始渲染所需的数据量上有很大的改进。
在每个视口外的图片标签上添加一个loading="lazy"
属性后,首次加载所需的数据量上会有很大的减少!
这导致更快的初始加载,后面的图片只有在用户滚动的时候才被加载。
这是一个很大的改进!但是如果图片是在css中无条件加载的,又该如何进行懒加载呢?
<div class="header-container">
<h2>blog.axiaoxin.com</h2>
</div>
<style>
.header-container {
background-image: url('bg.png');
}
</style>
loading=lazy
对于图片标签的src值很好用,但是背景图片呢?它们通常是通过background-image:url()
这样的css规则来声明的,目前还不能被loading
属性控制。我们怎么才能懒加载它们呢?
IntersectionObserver
这个基于JS的对象类似于以前那些绑定到滚动事件的JS插件。它仍然经常被用作loading=lazy
策略上面提到的一个替代实现。由于IntersectionObserver是直接在浏览器级别实现的,它比挂在滚动监听器上更有效率,但是概念上做的是同样的事情 - 等到元素X在视口内或者接近视口,然后做动作Y。
<!-- 在不支持loading="lazy"的浏览器中懒加载图片 -->
<img data-src="image.jpg" class="lazyload" />
<script>
function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
// 元素已经穿过了我们的观察
// 阈值 - 从data-src加载src
entry.target.src = entry.target.dataset.src;
entry.target.classList.remove('lazyload');
// 这个元素的任务完成了 - 不需要再观察它了!
observer.unobserve(entry.target);
}
});
}
const images = document.querySelectorAll('.lazyload');
const observer = new IntersectionObserver(handleIntersection);
images.forEach(image => observer.observe(image));
</script>
上面的代码会在图片进入视口的时候加载它的src。
这里有一个重要的注意事项,那就是我们通常不想等到一个图片容器在视口内才触发加载。加载图片需要时间,而且延迟对用户是可以感知的。理想的情况是,我们希望在图片容器接近屏幕,可能很快就会出现在视口,但是还没有到的时候,就开始图片的加载过程。为了做到这一点,我们可以使用IntersectionObserver
的rootMargin
。这是一个属性,它使用CSS值来有效地扩展容器的大小。
比如说,我们想让一个图片在离视口100px的时候开始加载(在任何方向 - 视口上方或者下方)。我们可以通过给我们的IntersectionObserver的初始化添加一个rootMargin
值来做到这一点。这个值的作用类似于CSS的padding或者margin指令 - 100px 100px 100px 100px
可以压缩成一个值100px
:
const observer = new IntersectionObserver(
handleIntersection,
{ rootMargin: "100px" }
);
我们怎么把这个应用到我们的背景图片问题上呢?
懒加载背景图片
我们来看一个div,它有一个背景图片bg.png
。我们可以做的是把这个div的CSS分成两个类 - 一个包含除了背景图片以外的所有属性(.header-container
),另一个只包含背景图片(.header-bg-img
)。
<div class="header-container">
<h2>blog.axiaoxin.com</h2>
</div>
<style>
.header-container {
height: 150px;
width: 300px;
font-weight: bold;
background-size:cover;
}
h2 {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
text-shadow: 2px 2px white;
transform: translate(-50%, -50%);
font-weight: bold;
}
.header-bg-img {
background-image: url('bg.png');
}
</style>
当交叉观察器交叉的时候:
- 给我们的主div添加bg-image类。这会触发背景图片的加载。
- 移除观察器。这可以防止重复地操作DOM。
if (entry.isIntersecting) {
// 添加有背景图片的类
entry.target.classList.add('header-bg-img');
// 这个元素的任务完成了 - 不需要再观察它了!
observer.unobserve(entry.target);
}
这个方法非常好用。但是在很多实现中,我们经常会有动态的背景图片,而且事先不知道它们,所以不能把它们添加到我们的打包好的CSS文件中。一个例子是为不同的div设置不同的背景图。有没有其他的方法呢?
使用数据属性来懒加载背景图片
我们可以通过使用data-
属性来支持一个更动态的分配背景图片的方法。这样我们就可以不用动我们的打包好的CSS文件,而是把任何需要的背景图片作为数据属性设置在相关的元素上。这改变了我们的过程:
- 给div添加一个数据属性,比如data-bgimage=x.png。
- 使用一个交叉观察器,当这个div穿过我们的阈值的时候,把这个data-bgimage的值设置为background-image:url()的值。
- 整理 - 取消观察已经应用了背景的元素。
<div class="header-container" data-bgimage="bg.png">
<h2>blog.axiaoxin.com</h2>
</div>
<style>
.header-container {
height: 150px;
width: 300px;
font-weight: bold;
background-size:cover;
}
h2 {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
text-shadow: 2px 2px white;
transform: translate(-50%, -50%);
font-weight: bold;
}
</style>
<script>
// 检查是否支持IntersectionObserver
if ('IntersectionObserver' in window) {
document.addEventListener("DOMContentLoaded", function() {
function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
// 元素已经穿过了我们的观察
// 阈值 - 从data-src加载src
entry.target.style.backgroundImage = "url('"+entry.target.dataset.bgimage+"')";
// 这个元素的任务完成了 - 不需要再观察它了!
observer.unobserve(entry.target);
}
});
}
const headers = document.querySelectorAll('.header-container');
const observer = new IntersectionObserver(
handleIntersection,
{ rootMargin: "100px" }
);
headers.forEach(header => observer.observe(header));
});
} else {
// 没有交互支持?自动加载所有背景图片
const headers = document.querySelectorAll('.header-container');
headers.forEach(header => {
header.style.backgroundImage = "url('"+header.dataset.bgimage+"')";
});
}
</script>
通过这种方法,我们可以懒加载背景图片,确保我们的初始页面加载快速且性能良好,同时不影响用户体验。
由于背景图片通常是比较大的,hero-style 的图片,也许是为了在桌面屏幕上拉伸而没有提供移动版,这种懒加载技术可以给页面加载速度指标带来显著的改进。