前端性能优化:图片延迟加载详解
前端开发的时候,有些列表或比较长的页面会存在有很多图片需要加载。一次加载太多图片,会占用很大的带宽,影响网页的加载速度。为提升用户体验,希望视觉窗口外的图像不会加载,让用户浏览到什么地方,就加载该处的图片。这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担,降低卡顿现象发生。
网页中如果存在许多图片资源,浏览器会一次性下载所有的图片资源,通常为自上而下依次加载。这样会造成两个问题:
流量浪费:还未出现在用户视野中的图片,不应当被加载;
网络阻塞:通常情况下,浏览器加载网络资源,最多只有6个并发资源下载;从而可能会导致阻塞JS代码资源的下载,造成网站的功能加载异常;
网页加载80%的响应时间都花在图片、样式、脚本等资源的下载上,而样式以及脚本的加载极为重要,他影响到网页正常使用;
因此,我们需要一种方案,对于那些含有大量图片的网页,实现仅当图片出现在用户视口区域时,浏览器才去加载图片资源,这种方案被称为图片延迟加载;
通过图片延迟加载方案,我们能够避免流量浪费。但网络阻塞的问题,并不仅仅是通过图片延迟加载方案解决的,图片懒加载仅能在一定程度上避免大量网络资源请求。
HTTP/1.1中,我们可以使用CDN实现域名分片机制,但如果使用HTTP/2则不需要关心这个问题,它采用了多路复用技术,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。
解决方案
图片延迟加载是一个很重要的前端性能优化手段,思路一般是预先加载一个尺寸很小的占位图片,然后再通过js选择性的修改src属性去加载真正的图片。目前实现手段基本分为三种:
方案一:浏览器原生支持,element的loading="lazy"属性;
方案二:监听图片元素是否可见(IntersectionObserver API);
方案三:监听到scroll事件,计算图片在视觉窗口位置
浏览器原生支持
<img src="./example.jpg" loading="lazy" alt="loading lazy">
loading属性可用于iframe标签和img标签;
eager默认值:当loading属性的默认值为eager,即立即请求资源,即当你不设置loading='lazy'时,或者loading="无效值"时,均代表立即请求当前资源;
lazy:代表将延迟加载当前element,但如果页面禁止了JavaScript的运行,则也不会生效,这是浏览器的一种反追踪措施;
注意兼容性,谷歌内核从77开始完全支持。也就是近几年的事。
使用代码实现
IntersectionObserver API实现
IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。
简单来说,IntersectionObserver API,可以自动"观察"元素是否可见。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做 交叉观察器。
IntersectionObserver在懒加载、虚拟滚动、曝光统计、上拉刷新等场景中,均能提供高效的解决方案。因为传统的 观察元素是否可见方案,都离不开Element.getBoundingClientRect等DOM方法,而这些方法均运行在浏览器主线程,一旦方案设计有缺陷,去频繁的触发调用,便会造成一定的性能问题。
兼容性说明
Chromium: Shipped in Chrome 51
Edge: Shipped in build 14986
Firefox: Shipped in Firefox 55
WebKit: Shipped in Safari 12.1 and iOS 12.2
<img data-src="image.jpg" alt="test image">
<script type="text/javascript">
const config = {
rootMargin: '0px 0px 50px 0px',
threshold: 0
};
const preloadImage = (imagEl) => {
if (imagEl.getAttribute('src') !== imagEl.getAttribute('data-src')) {
imagEl.src = imagEl.getAttribute('data-src');
}
};
let observer = new intersectionObserver(function(entries, self) {
entries.forEach(entry => {
if(entry.isIntersecting) {
// 将 data-src 改到 src
preloadImage(entry.target);
// 停止对它监听
self.unobserve(entry.target);
}
});
}, config);
const imgs = document.querySelectorAll('[data-src]');
imgs.forEach(img => {
observer.observe(img);
});
</script>
传统的实现方法
监听到scroll事件,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。
再动态修改src属性加载图片。
<body>
<style>
img {
display: block;
margin-bottom: 50px;
height: 200px;
}
</style>
<img src="images/placeholder.jpg" data-src="images/1.png">
<img src="images/placeholder.jpg" data-src="images/2.png">
<img src="images/placeholder.jpg" data-src="images/3.png">
<img src="images/placeholder.jpg" data-src="images/4.png">
<img src="images/placeholder.jpg" data-src="images/5.png">
<img src="images/placeholder.jpg" data-src="images/6.png">
<img src="images/placeholder.jpg" data-src="images/7.png">
<img src="images/placeholder.jpg" data-src="images/8.png">
<img src="images/placeholder.jpg" data-src="images/9.png">
<img src="images/placeholder.jpg" data-src="images/10.png">
<img src="images/placeholder.jpg" data-src="images/11.png">
<img src="images/placeholder.jpg" data-src="images/12.png">
<script>
function throttle(fn, delay, atleast) {
var timeout = null;
var startTime = new Date();
return function () {
var curTime = new Date();
clearTimeout(timeout);
if (curTime - startTime >= atleast) {
fn();
startTime = curTime;
} else {
timeout = setTimeout(fn, delay);
}
}
}
function lazyload() {
var images = document.querySelectorAll('[data-src]');
var len = images.length;
var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
return function () {
var seeHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for (var i = n; i < len; i++) {
if (images[i].offsetTop < seeHeight + scrollTop) {
if (images[i].getAttribute('src') !== images[i].getAttribute('data-src')) {
images[i].src = images[i].getAttribute('data-src');
}
n = n + 1;
}
}
}
}
var loadImages = lazyload();
loadImages(); //初始化首页的页面图片
// window.addEventListener('scroll', loadImages, false); //会被高频触发,这非常影响浏览器的性能
window.addEventListener('scroll', throttle(loadImages, 500, 1000), false); //设置500ms 的延迟,和 1000ms 的间隔 避免高频防抖
</script>
</body>
针对这个方案,有可以用现成的库。CoreNext则调用的这个库vanilla-lazyload
https://github.com/verlok/vanilla-lazyload
用起来非常简单
首先,给img标签,src默认一个占位图,data-src为真实图片地址。
<img alt="A lazy image" class="lazy" data-src="lazy.jpg" />
在JS里面,调用这个类,即可实现延迟加载。
let LazyLoad = new LazyLoad();
当然除了这个,针对页面的一些元素,如果通过动态更改,则需要手动更新
LazyLoad.update();
除了这个库,同类的也有一大把,根据需要调用即可。
方案对比
通过这三种方式可以看出图片加载的实现方案,但以上代码还不能很好的投入到生产环境。原因如下:
方案一:使用简单但存在主流浏览器市场占用率问题,对要适配其它平台面临比较严峻的兼容性问
方案二:实施起来有效,并且使 intersectionObserver在计算方面能够承担繁重的工作。虽然大多数浏览器都支持IntersectionObserver API的最新版本,但并非所有浏览器都始终支持该API。 幸运的是,可以使用polyfill。
方案三:传统方式。主流浏览器都支持,但在列表里如果不加防抖在列表页快速滑动的操作中也会有卡顿现象,加上防抖时会有短暂视觉延迟。
传统方式遇到的问题,库里面都提供了解决方案,如果考虑自己写,则需要注意一些问题。
推荐开源项目
图片延迟加载、响应式图片等细节诸多细节,如果想做一款功能比较齐全,兼容性较好还是要付出不小的努力。所幸市面有不好的开源项目,很做了很多这方面的处理。
开源项目 | Star | 推荐 |
---|---|---|
vue-lazyload | 7k | 符合Vue开发习惯,常用功能比较全,支持响应式图片。vue用户首选。 |
lazysizes | 17K | 功能比较齐全,历史悠久,星数较高,支持响应式图片。 |
vanilla-lazyload | 7k | 体积较小2.4 kB 功能全 |
react-lazyload | 6K | 符合react用户群体 |