什么是图片懒加载?
如今一张图片的大小可以轻松达到几M
的大小,如果一个页面中同时有比较多这样的图片,同时加载的话会造成页面加载缓慢,当目标图片未加载成功时,通常会使用一个小图片占位,例如一个转圈圈的gif来表示图片正处于loading
状态,但这改变不了很多图片同时加载带来的加载缓慢的问题。
因此很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载,这样对页面的加载性能有较大提升,同时也改善了用户体验。
主要实现思路如下:
1、img
元素的自定义属性上data-xxx
上挂载目标图片url
, src
属性指向默认图片地址。
2、监听图片是否出现在用户的可视区域内。
3、出现在可视区域内后更新src
属性为目标图片url
。
以下两种实现方式的区别主要体现在第二步。
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
属性上是目标图片的地址

在此方法中懒加载的核心逻辑为:监听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
的值,加载目标图片。
让我们来看一下使用效果:

可以看出,当鼠标滚动时,首先加载在当前视口内的图片,当新的图片进入视口内时,加载新的图片,所有图片并没有同时进行加载,达到了预期效果。
但是,直接将函数绑定在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))
|
在线地址
但是!!!


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