0%

手把手教你实现图片懒加载


什么是图片懒加载?

如今一张图片的大小可以轻松达到几M的大小,如果一个页面中同时有比较多这样的图片,同时加载的话会造成页面加载缓慢,当目标图片未加载成功时,通常会使用一个小图片占位,例如一个转圈圈的gif来表示图片正处于loading状态,但这改变不了很多图片同时加载带来的加载缓慢的问题。

因此很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载,这样对页面的加载性能有较大提升,同时也改善了用户体验。


主要实现思路如下:

1、img元素的自定义属性上data-xxx上挂载目标图片url, src属性指向默认图片地址。

2、监听图片是否出现在用户的可视区域内。

3、出现在可视区域内后更新src属性为目标图片url

以下两种实现方式的区别主要体现在第二步。

监听scroll事件+节流 实现

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 http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div></div>
</body>
<style>
div {
display: flex;
justify-content: center;
flex-direction: column;
}
.img {
width: 50%;
height: 50%;
}
</style>
<script>
const loadingSrc = 'https://c.tenor.com/j-sGh5ZtdaEAAAAi/hongfei-fei.gif'
const srcs = [
'https://w.wallhaven.cc/full/wq/wallhaven-wq855r.png',
'https://w.wallhaven.cc/full/6o/wallhaven-6ooo66.png',
'https://w.wallhaven.cc/full/eo/wallhaven-eoqk88.png',
'https://w.wallhaven.cc/full/8x/wallhaven-8xlx2o.png',
'https://w.wallhaven.cc/full/q2/wallhaven-q2drrl.jpg',
'https://w.wallhaven.cc/full/ym/wallhaven-ymrrex.png',
'https://w.wallhaven.cc/full/mp/wallhaven-mp8wkm.png',
'https://w.wallhaven.cc/full/dg/wallhaven-dgzp2m.jpg',
'https://w.wallhaven.cc/full/lq/wallhaven-lqek12.png',
'https://w.wallhaven.cc/full/r7/wallhaven-r7mkzq.jpg',
'https://w.wallhaven.cc/full/qd/wallhaven-qd12jl.png',
'https://w.wallhaven.cc/full/0q/wallhaven-0q23r5.jpg',
'https://w.wallhaven.cc/full/76/wallhaven-76wj9o.png',
'https://w.wallhaven.cc/full/5d/wallhaven-5d6gd5.jpg',
'https://w.wallhaven.cc/full/gj/wallhaven-gjmd9q.png',
'https://w.wallhaven.cc/full/pk/wallhaven-pk91m3.png',
'https://w.wallhaven.cc/full/pk/wallhaven-pk9zm9.png',
'https://w.wallhaven.cc/full/j8/wallhaven-j85wv5.png',
]

for (let src of srcs) {
let imgNode = document.createElement('img')
document.body.appendChild(imgNode)
imgNode.src = loadingSrc
imgNode.className = 'img'
imgNode.setAttribute('data-src', src)
}

let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
</script>
</html>

渲染后的结构如下图所示,src属性指向一张默认加载图片的地址,data-src属性上是目标图片的地址
image.png
在此方法中懒加载的核心逻辑为:监听scroll事件,对比容器的高度、滚动高度、和图片距离容器顶部的高度,判断是否滚动到可视区域

主要实现如下:

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
const imgLazyLoad = (function () {
let count = 0
return function () {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
console.log(`加载第${index}张图片`)
img.src = img.dataset.src
deleteIndexList.push(index)
count++
}
// 说明图片已经全部加载完成了,移除监听事件
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
})
// 剔除已经完成加载的图片的地址
imgList = imgList.filter(
(img, index) => !deleteIndexList.includes(index)
)
}
})()
// 监听鼠标滚动事件
document.addEventListener('scroll', imgLazyLoad)

Element.getBoundingClientRect() 返回元素的大小以及相对于视口的位置.
window.innerheight 是视口的高度.
img.top < window.innerHeight时说明该图片元素正处于视口之内,应该进行加载,将img.src的值更换为img.dataset.src的值,加载目标图片。

让我们来看一下使用效果:
CPT2204051626-585x381.gif
可以看出,当鼠标滚动时,首先加载在当前视口内的图片,当新的图片进入视口内时,加载新的图片,所有图片并没有同时进行加载,达到了预期效果。
但是,直接将函数绑定在scroll事件上,当页面滚动时,函数会被高频触发。因此我们再使用节流函数优化一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function throttle(func, wait) {
let prev = 0,
context,
args
return function () {
let now = +new Date()
context = this
args = arguments
if (now - prev > wait) {
func.apply(context, args)
prev = now
}
}
}
// 监听鼠标滚动事件
document.addEventListener('scroll', throttle(imgLazyLoad, 500))

在线地址

但是!!!
imag

CPT2204051626-585x381.gif
getBoundingClientRect返回最新的位置信息,会触发回流重绘以返回正确值

getBoundingClientRect一样会触发回流重绘的还有:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle() 具体请看这里

这里使用了节流减少了回调函数的调用次数,但有没有办法不触发回流重绘也能检测到图片处于当前视口呢?这就是接下来要介绍的intersectionObserver API

使用 intersectionObserver API 实现

IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象
其中:isIntersecting表示目标是否可见,即是否在视口中
具体可见MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function imgLazyLoad() {
const io = new IntersectionObserver((inters) => {
inters.forEach((item, index) => {
// 进入可视区域
if (item.isIntersecting) {
item.target.src = item.target.dataset.src
//停止监听该元素
io.unobserve(item.target)
}
})
})
// 监听每一个元素
imgList.forEach((el) => io.observe(el))
}

相比于使用getBoundingClientRect,实现了相同的懒加载效果,但避免了不必要的回流重绘。

在线地址

参考

前端性能优化之图片懒加载

实现图片懒加载(Lazyload)

MDN - Intersection Observer