Fork me on GitHub

懒加载

懒加载

网页性能优化已是老生常谈了,其实性能优化最大的关键在于什么呢,影响网页性能罪魁祸首必定是图片加载了。所以图片加载优化势在必行。图片加载优化我们可以选择懒加载(可视区域加载)。

懒加载是一种对网页性能优化的方式,它的原理是优先加载在可视区域内的图片,而不一次性加载所以图片。当浏览器滚动,图片进入可视区时再去加载图片。通过设置图片的 src 属性来让浏览器发起图片的请求。当这个属性为空或者没有时,就不会发送请求。

懒加载是一种对网页性能优化的方式,它的原理是优先加载在可视区域内的图片,而不一次性加载所以图片。当浏览器滚动,图片进入可视区时再去加载图片。通过设置图片的 src 属性来让浏览器发起图片的请求。当这个属性为空或者没有时,就不会发送请求。

实现

懒加载的实现主要是判断当前图片是否到了可视区域这一核心逻辑。我们先来整理一下实现思路:

  1. 拿到所有的图片 img dom
  2. 遍历每个图片判断当前图片是否到了可视区范围内。
  3. 如果到了就设置图片的 src 属性。
  4. 绑定 window 的 scroll 事件,对其进行事件监听。

HTML 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="container">
<div class="img-area">
<img id="first" data-src="./img/ceng.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/data.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/huaji.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/liqi1.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/liqi2.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/steve-jobs.jpg" alt="">
</div>
</div>

此时 img 标签是没有 src 属性的,我们把真实的图片地址放在一个属性里,这里我们使用 HTML5 的 data 属性,将真实地址放在自定义的 data-src 中。

判断图片是否进入了可视区域

这一逻辑有两种方法,听我娓娓道来。

方法一

第一种方法我们通过计算该图片距离 document 顶部的高度是否小于当前可视区域相对于 document 顶部高度来判断。

可视区域相对于 document 顶部高度的计算方法:

1
2
const clientHeight = document.documentElement.clientHeight; // 视口高度,也就是窗口的高度。
const scrollHeight = document.documentElement.scrollTop + clientHeight; // 滚动条偏移文档顶部的高度(也就是文档从顶部开始到可视区域被抹去的高度) + 视口高度

画了一张图方便理解:

高度

然后就是计算该图片距离文档顶部的高度。有两种方法,第一种方法是通过元素的 offsetTop 属性来计算。从上图我们了解到元素的 offsetTop 属性是相对于一个 position非 static 的祖先元素,也就是 child.offsetParent 。同时需要将祖先元素的 border 考虑在内,我们通过child.offsetParent.clientTop可以拿到边框厚度。

由此我们得到元素距离文档顶部的高度的计算方法:

1
2
3
4
5
6
7
8
9
function getTop(el, initVal) {
let top = el.offsetTop + initVal;
if (el.offsetParent !== null) {
top += el.offsetParent.clientTop;
return getTop(el.offsetParent, top);
} else {
return top;
}
}

这里的这个方法使用了 尾递归调用 。可以提高递归性能。当然这里也可以用循环来实现:

1
2
3
4
5
6
7
8
9
function getTop(el) {
let top = el.offsetTop;
var parent = el.offsetParent;
while(parent !== null) {
top += parent.offsetTop + parent.clientTop;
parent = parent.offsetParent;
}
return top;
}

第二种方法是使用 element.getBoundingClientRect() API 直接得到 top 值。

getBoundingClientRect 的返回值如下图:

rect

1
2
3
var first  = document.getElementById('first');
getTop(first, 0); // 130
console.log(first.getBoundingClientRect().top); // 130

于是我们得到判断方法:

1
2
3
4
5
6
7
8
function inSight(el) {
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollTop + clientHeight;
// 方法一
return getTop(el, 0) < scrollHeight;
// 方法二
// return el.getBoundingClientRect().top < clientHeight;
}

接下来就是对每个图片进行判断和赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadImg(el) {
if (!el.src) {
el.src = el.dataset.src;
}
}

function checkImgs() {
const imgs = document.getElementsByTagName('img');
Array.from(imgs).forEach(el => {
if (inSight(el)) {
loadImg(el);
}
})
console.log(count++);
}

最后给 window 绑定 onscroll 事件以及 onload 事件:

1
2
window.addEventListener('scroll', checkImgs, false);
window.onload = checkImgs;

我们知道类似 scrollresize 这样的事件浏览器可能在很短的时间内触发很多次,为了提高网页性能,我们需要一个节流函数来控制函数的多次触发,在一段时间内(如 500ms)只执行一次回调。

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
/**
* 持续触发事件,每隔一段时间,只执行一次事件。
* @param fun 要执行的函数
* @param delay 延迟时间
* @param time 在 time 时间内必须执行一次
*/
function throttle(fun, delay, time) {
var timeout;
var previous = +new Date();
return function () {
var now = +new Date();
var context = this;
var args = arguments;
clearTimeout(timeout);
if (now - previous >= time) {
fun.apply(context, args);
previous = now;
} else {
timeout = setTimeout(function () {
fun.apply(context, args);
}, delay);
}
}
}
window.addEventListener('scroll', throttle(checkImgs, 200, 1000), false);

方法二

HTML5 有一个新的 IntersectionObserver API,它可以自动观察元素是否可见。

主要用法:

1
2
3
4
5
6
7
8
9
10
var observer = new IntersectionObserver(callback, option);

// 开始观察
observer.observe(document.getElementById('first'));

// 停止观察
observer.unobserve(document.getElementById('first'));

// 关闭观察器
observer.disconnect();

目标的可见性发生变化时就会调用观察器的 callback。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function callback(changes: IntersectionObserverEntry[]) {
console.log(changes[0])
}

// IntersectionObserverEntry
{
time: 29.499999713152647,
intersectionRatio: 1,
boundingClientRect: DOMRectReadOnly {
bottom: 144,
height: 4,
left: 289,
right: 293,
top: 140,
width: 4,
x: 289,
y: 140
},
intersectionRect: DOMRectReadOnly,
isIntersecting: true,
rootBounds: DOMRectReadOnly,
target: img#first
}

详细释义:

  • time: 可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • intersectionRatio: 目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1 ,完全不可见时小于等于 0
  • boundingClientRect: 目标元素的矩形区域的信息
  • intersectionRect: 目标元素与视口(或根元素)的交叉区域的信息
  • rootBounds: 根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null
  • isIntersecting: 是否进入了视口,boolean 值
  • target: 被观察的目标元素,是一个 DOM 节点对象

使用 IntersectionObserver 实现图片懒加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function query(tag) {
return Array.from(document.getElementsByTagName(tag));
}
var observer = new IntersectionObserver(
(changes) => {
changes.forEach((change) => {
if (change.intersectionRatio > 0) {
var img = change.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
})
}
)
query('img').forEach((item) => {
observer.observe(item);
})