图片懒加载
# 图片懒加载
# 优点
提高前端性能,按需加载图片减轻服务器负担,提高页面加载速度。
# 原理
图片的加载是依赖于 src 路径,我们可以设置一个暂存器,把图片路劲放到暂存器中,当我们需要这个图片加载显示(进入可视区域)时,再把路径赋值给 src,这样就能实现按需加载,也就是懒加载。我们通常使用 html5 中的 data-属性作为暂存器,例如 src 的值默认是正在加载中的 GIF,而真正的图片路径是保存在 data-中。
问题拆分:
- 如何判断图片出现在了当前视口 (即如何判断我们能够看到图片)
(1)window.scroll 监听 Element.getBoundingClientRect() 并使用 throttle 节流
(2)IntersectionObserver(不支持 IE):一个能够监听元素是否到了当前视口的事件,一步到位!
const observer = new IntersectionObserver((changes) => {
// changes: 目标元素集合
changes.forEach((change) => {
// intersectionRatio
if (change.isIntersecting) {
const img = change.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
observer.observe(img);
2
3
4
5
6
7
8
9
10
11
12
13
- 如何控制图片的加载
首先设置一个临时 Data 属性 data-src,控制加载时使用 src 代替 data-src,可利用 DataSet API 实现img.src = img.datset.src
data-*
全局属性 是一类被称为自定义数据属性的属性,它赋予我们在所有 HTML 元素上嵌入自定义数据属性的能力,并可以通过脚本在 HTML 与 DOM 表现之间进行专有数据的交换。
# 图片懒加载方案
# 1.位置计算+scroll+DataSet
document.documentElement.clientHeight //获取屏幕可视区域的高度
document.documentElement.scrollTop //获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离
element.offsetTop //获取元素相对于文档顶部的高度
如果:clientHeight + scroolTop > offsetTop,则图片进入了可视区内,则被请求。
const imgs = document.getElementsByTagName("img");
const viewHeight = document.documentElement.clientHeight; // 视图高度
let count = 0; // 计数器,表示当前更新了几个,也可以作为下一个要更新元素的下标
const noop = () => {}; // 空函数
let lazyLaodWhtiThrottle = noop; // 带有节流功能的懒加载,初始化
// 懒加载
const lazyLaod = () => {
if (count >= imgs.length) {
// 加载完,移除事件
window.removeEventListener("scroll", lazyLaodWhtiThrottle);
}
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop; // 滚动距离
// 判断每个图片是否在视图内
// 是的话则加载真正的图片,count 加 1
// 遍历超出视图底部的元素则跳出循环,没有必要再找下去
// 如果图片没有 data-src, 则跳过,count 加 1
for (let i = count; i < imgs.length; i++) {
const img = imgs[i];
const dataSrc = img.getAttribute("data-src");
// 视图以下元素在当前不再遍历
if (img.offsetTop > scrollTop + viewHeight) {
break;
}
// 没有 data-src,不参与懒加载,程序应该跳过
if (!dataSrc) {
count++;
continue;
}
img.removeAttribute("data-src");
img.src = dataSrc;
count++;
}
};
// 使用节流
lazyLaodWhtiThrottle = _.throttle(lazyLaod, 200);
window.addEventListener("scroll", lazyLaodWhtiThrottle);
lazyLaod(); // 初始
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
# 2.getBoundingClientRect + scroll+DataSet
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
const imgs = document.getElementsByTagName("img");
let count = 0; // 计数器,表示当前更新了几个,也可以作为下一个要更新元素的下标
const noop = () => {}; // 空函数
let lazyLaodWhtiThrottle = noop; // 带有节流功能的懒加载,初始化
const lazyLaod = () => {
if (count >= imgs.length) {
window.removeEventListener("scroll", lazyLaodWhtiThrottle);
}
for (let i = count; i < imgs.length; i++) {
const img = imgs[i];
const dataSrc = img.getAttribute("data-src");
// 获取相对于视图窗口顶部的距离
const { top } = img.getBoundingClientRect();
// 如果大于可视区域高度,说明在视图窗口之下,跳出
if (top > document.documentElement.clientHeight) {
break;
}
// 不参与懒加载的元素, count + 1,进入下一个循环
if (!dataSrc) {
count++;
continue;
}
img.removeAttribute("data-src");
img.src = dataSrc;
count++;
}
};
lazyLaodWhtiThrottle = _.throttle(lazyLaod, 200);
window.addEventListener("scroll", lazyLaodWhtiThrottle);
lazyLaod();
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
# 3. IntersectionObserver
const imgs = document.getElementsByTagName("img");
function lazyLaod() {
const observer = new IntersectionObserver((res) => {
for (let i = 0; i < res.length; i++) {
const currentChange = res[i];
const { target } = currentChange;
const dataSrc = target.getAttribute("data-src");
if (!currentChange.isIntersecting) continue;
if (!dataSrc) {
observer.unobserve(target);
continue;
}
target.src = dataSrc;
target.removeAttribute("data-src");
observer.unobserve(target);
}
});
for (let i = 0; i < imgs.length; i++) {
observer.observe(imgs[i]);
}
}
lazyLaod();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
代码解析:
实例化一个 IntersectionObserver 实例,注册回调函数。
for 循环遍历所有图片,进行观察
针对回调的结果集做判断和处理,最后记得要注销改元素的观察
# 4.基于 img 标签的 loading 属性实现懒加载
前面讲的三种方案都是基于 javaScript 来进行元素位置判断,动态替换 src 来实现图片懒加载的效果。那么,有没有一种办法可以更加干脆,而不用这么多逻辑判断,并且性能更佳。答案是,有的。它就是 img 标签上原生支持的属性 loading。
我们可以很方便的使用 loading="lazy" 的方式来实现一个图片懒加载的功能,并且不依赖我们的 javaScript, 仅仅依靠原生支持以及浏览器的优化,性能上自然不必多说。
然而,非常遗憾的是,在 chrome 上,也在 75 版本之后才有支持,兼容性简直令人望而却步。因此,如果我们想要首先使用原生支持的 lazy-loading, 依然需要配合 javaScript 做一个可用性判断,以决定当前程序要采用何种懒加载方式。
这里我们可以采用两种方式,第一种就是丝毫不借助 javaScript,上来就是干,只需要在 img 标签上加上 loading="lazy" 即可。
<img src="https://xxxx/xxx.png" loading="lazy" />
当然,因为兼容性的问题,我们也可以在 avaScript 中做一层判断,这样,我们可以采用前面几种方案相同 HTML 结构, 依然给 img 标签赋予 src 和 data-src 两个资源。
const imgs = document.getElementsByTagName("img");
function lazyLaod() {
// 判断是否支持 loading
if ("loading" in HTMLImageElement.prototype) {
for (let i = 0; i < imgs.length; i++) {
const img = imgs[i];
const dataSrc = img.getAttribute("data-src");
if (!dataSrc) continue;
img.loading = "lazy";
img.src = dataSrc;
img.removeAttribute("data-src");
}
}
}
lazyLaod();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
代码解析:
添加 loading = "lazy"
将 data-src 赋值给 src
# 方案总结
针对以上 4 种方案,在性能体验以及兼容性上各有利弊。或许我们惊喜于原生支持的方案,免去了很多繁杂写法和拥有更优的性能体验,但兼容性却不容乐观,相反,以往看似不过太靠谱的方案在兼容性方面却显得不错。如果我们的程序希望优先选择性能最佳的懒加载方式,完全可以通过策略模式,在当前浏览器环境使用中判断属性支持程度以动态切换懒加载策略,在支持度不高的浏览器中也能降级为兼容性方案。
本文细数了 4 种前端图片懒加载的实现方案,其中,位置计算类型的共通思想都是计算元素相对于可视区域的位置,只不过基于 offsetTop + clientHeight + scrollTop 的方案中,位置的计算借助了元素的偏移距离、文档的滚动距离以及可视区域高度,而基于 getBoundingClientRect 的方案则是通过这个 API 来获取相对于视口左上角的位置。两者相比,后者更加清晰、简单。
而 Intersection Observer API 的方案,避免了前两种方案位置计算的步骤,只在交集检测的结果集进行操作,当交集变化的时候,回调函数将会被调用,通过判断结果集 isIntersecting 属性即可。
最后一个方案,则是基于 img 标签原生支持的功能,通过设置 loading="lazy" 来实现图片懒加载,不需要借助 js 计算,仅仅依靠原生支持以及浏览器优化,性能更佳。但是兼容性方面目前来讲非常糟糕。