图片懒加载的原理是没有在可视区域的图片暂时不加载图片,等进入可视区域后在加载图片,这样可以减少初始页面加载的图片数量而提升页面加载速度。 图片懒加载在提升页面加载速度的同时也会伴随用户看其他未展示的图片时会有等待时间;图片加载显示会伴有布局抖动等问题。

方案

图片懒加载的关键是:判断一个元素是否在可视区域。

img的loading属性设为“lazy”

HTMLImageElement 的 loading 属性为一个字符串,它的值会提示 用户代理 告诉浏览器不在可视视口内的图片该如何加载。这样一来,通过推迟图片加载仅让其在需要的时候加载而非页面初始载入时立刻加载,优化了页面的载入。
lazy 告诉用户代理推迟图片加载直到浏览器认为其需要立即加载时才去加载。例如,如果用户正在往下滚动页面,值为 lazy 会导致图片仅在马上要出现在 可视视口中时开始加载。

使用方法

1
2
<img src="xxx.jpg" loading="lazy" />  

缺点

虽然整个方案简单性能好,但问题也是最多的,所以很少使用这种方案。

  1. 前面提到图片懒加载用户看其他未展示的图片时会有等待时间,一般会设置一个默认图片,这种方案不能设置默认图片
  2. 图片的加载数量和图片的布局、可视区域尺寸有关,难以控制
  3. 图片的加载顺序也难以控制

该方案能粗略的实现图片懒加载基本功能。

通过offsetTop来计算是否在可视区域内

可视区域高度是 document.documentElement.clientHeight ,而可视区域的位置是在滚动条滚动位置 scrollTopscrollTop+document.documentElement.clientHeight之间。因此通过 image.offsetTop <= document.documentElement.clientHeight + document.documentElement.scrollTop 判断图片是否可以在可视区域内。

1
2
3
4
5
6
7
8
9
10
11
12
13
function lazyload() {
var lazyImages = document.querySelectorAll(".lazyload");
lazyImages.forEach(function (image) {
//如果元素距离文档顶部的高度小于浏览器的滚动高度加浏览器的可视高度,则需要加载
if ( image.offsetTop <= document.documentElement.clientHeight + document.documentElement.scrollTop) {
image.src = image.getAttribute("data-src");
}
});
}
window.onscroll = function () {
lazyload();
};

1
2
<img src="./default.gif" class="lazyload" data-src="./photo-1.jpg" />

采用getBoundingClientRect

原理都是一样的,只不过是判断元素出现在可视范围内的方式不同。

getBoundingClientRect用于获取其相对于视口的位置,这种方式更容易理解。

该api返回值是一个 DOMRect对象,拥有left, top, right, bottom, x, y, width, 和 height属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();

return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
);
}

Intersection Observer

IntersectionObserver的作用是监听目标元素与祖先元素是否有相交(默认是浏览器的视口),如果相交就触发事件。使用IntersectionObserver完成懒加载,优势在于我们不用再去计算元素距离判断图片是否已经出现在视口了,当图片出现在视口内时它会自动触发回调,完成我们回调内部的业务逻辑。

使用步骤主要分为两步:创建观察者和传入被观察者

创建观察者

1
2
3
4
5
6
7
8
9
10
11
const options = {
 // 表示重叠面积占被观察者的比例,从 0 - 1 取值,
 // 1 表示完全被包含
 threshold: 1.0,
 root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};

const callback = (entries, observer) => { ....}

const observer = new IntersectionObserver(callback, options);

通过new IntersectionObserver创建了观察者 observer,传入的参数 callback 在重叠比例超过 threshold 时会被执行`

关于callback回调函数常用属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 上段代码中被省略的 callback
const callback = function(entries, observer) {
   entries.forEach(entry => {
       entry.time;               // 触发的时间
       entry.rootBounds;         // 根元素的位置矩形,这种情况下为视窗位置
       entry.boundingClientRect; // 被观察者的位置举行
       entry.intersectionRect;   // 重叠区域的位置矩形
       entry.intersectionRatio;  // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
       entry.target;             // 被观察者
  });
};

传入被观察者

通过 observer.observe(target) 这一行代码即可简单的注册被观察者

1
2
3
const target = document.querySelector('.target');
observer.observe(target);

举例

假如我们需要实现图片加载前loading的效果的话,我们就可以给img的src设为loading图片的路径,data-src设为图片真实路径,在上述callback函数,将data-src的值赋给src即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!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>
#app {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}

.img {
height: 300px;
margin: 10px;
}
</style>
</head>

<body>
<div id="app">
<img class="img" src="./loading.gif"
data-src="https://p26-passport.byteacctimg.com/img/user-avatar/fd965033fba9d2b1b67d0dd6d1c80ad3~150x150.awebp"
alt="">
<!-- ......此处省略一堆img -->
</div>
</body>
<script>
const config = {
root:null, //监听元素的祖先元素dom对象,其边界盒将被视作视口。默认为浏览器视口
rootMargin:'0px 0px 0px 0px', // 距离视口多远触发回调函数
threshold:'0' // 目标元素与设置root元素相交的比例,若指定值为 1.0,则意味着整个元素都在可见范围内时才触发回调
}
var observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// isIntersecting为true说明出现在视口了
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
// 更改过地址的img 就取消监听 优化性能
observer.unobserve(entry.target)
}
})
},config );

const imgs = document.querySelectorAll('img')

// 使用observer.observe监听所有的img
imgs.forEach(img => {
observer.observe(img)
})

</script>

</html>

优点

Intersection Observer的优点在于不需要对事件进行监听,而上面两个方法都需要对类似于scroll事件进行监听,如果渲染列表很长,有可能会造成页面卡顿,因为scroll事件伴随了大量的计算,会造成资源方面的浪费