前言
当我们在导航栏上输入我们性感的 url
并按下回车的时候,浏览器其实做了非常多的工作,撇去复杂细节不讲,直观上就包括了对资源的请求和下载,以及视图的渲染和呈现。假设我们有一个多图文的新闻资讯类的网站,在网络请求的时候,想要一次性获取所有的图片并加载展示出来,那体验肯定是非常糟糕的。为什么?
首先我们来看看一个图片在 HTML
中是如何表现的:
<img src="https://xxxx/xxx.png"/>
这其中对于图片资源的引用,我们用到了属性 src
。
src
, 实际上是 sourc
的简称,主要用于替换当前内容。当浏览器执行到 src
的时候,就会暂停其他资源的下载和处理,直到当前的资源加载和执行完毕,这也就是为什么我们的 script
标签尽量应该放在底部的原因。
因此,回到我们上面多图文新闻资讯类网站的假设上,解决多图文呈现的性能和体验问题,最常用的方法就是对资源的下载和处理做一些限制,既图片懒加载。
什么是图片懒加载?
图片懒加载,顾名思义,就是懒。那么懒到何种程度?简单来说,就是你翻不到的,我都可能懒得去给你请求,更谈不上渲染呈现给你看了。这样子做的好处,一方面我们避免了一时间加载过多图片造成的网络带宽压力,另一方面也让前端界面加载更快,体验更佳。
基本思想
初始化
img
标签的 src
属性上指向默认占位图,src
属性则指向真正图片的 url
。
视图 scroll 或者 resize
将视图窗口内的所有 img
标签的 src
赋值给 src
,完成当前图片的加载。
结束
所有的图片都加载完毕,移除 scroll / resize
事件,或者移除相关懒加载监控事件。
图片懒加载实现方案
关于图片懒加载的实现方案,我将其分为三类,分别是视口位置计算、交集观察以及 HTML 原生支持。
其中, 视口位置计算方案有两个,分别是:
- 基于
offsetTop
+clientHeight
+scrollTop
实现懒加载 - 基于
getBoundingClientRect
实现懒加载
交集观察法:
- 基于
Intersection Observer API
实现懒加载
HTML 原生支持:
- 基于
img
标签的loading
属性实现懒加载
接下来,分别实现以上 4
种方案来实现图片懒加载。
基于 offsetTop
+ clientHeight
+ scrollTop
实现懒加载
原理
offsetTop
: 只读属性。代表着一个元素相对于其 offsetParent
的元素顶部内边距的距离。
scrollTop
:可以获取一个元素内容垂直滚动的像素数。简单说来,一个元素的 scrollTop
可以代表这个元素的内容顶部到视口顶部的距离,即滚动距离。
clientHeight
:元素可视区域范围高度。
在每次触发懒加载计算的时候,程序会对所有的图片都进行判断,如果符合公式 img.offsetTop < document.documentElement.scrollTop + document.documentElement.clientHeight
, 即元素偏移距离小于文档滚动高度和视图高度的和,说明元素出现在当前视图底部的上方,那我们就执行替换真正图片的程序,否则跳过不作处理。
代码实现
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
// 遍历超出视图底部的元素则跳出循环,没有必要再找下去
// 如果图片没有 src, 则跳过,count 加 1
for (let i = count; i < imgs.length; i ++) {
const img = imgs[i]
const dataSrc = img.getAttribute('src')
// 视图以下元素在当前不再遍历
if (img.offsetTop > scrollTop + viewHeight) {
break
}
// 没有 src,不参与懒加载,程序应该跳过
if (!dataSrc) {
count ++
continue
}
img.removeAttribute('src')
img.src = dataSrc
count ++
}
}
// 使用节流
lazyLaodWhtiThrottle = _.throttle(lazyLaod, 200)
window.addEventListener('scroll',lazyLaodWhtiThrottle)
lazyLaod() // 初始
代码解析
对照上面代码来讲:
count
, 一个计数器,代表当前更新了imgs
数组中多少个元素了。在我们的懒加载中,也会当做下一个要更新元素的下标传给i
。
javascript for (let i = count; i < imgs.length; i ++){ // ... }
其次,当 `count` 大于或者等于 `imgs` 的长度之后,说明所有元素都已经遍历过,则应该清除滚动事件。
- 在遍历元素上除了基于
count
的第一个元素下标的优化,也应该停止视图之下(可见区域后面)的元素的遍历,因为剩下的元素都是在视图之外,我们并不需要更新。
javascript if (img.offsetTop > scrollTop + viewHeight) { break }
- 当遍历的元素没有
src
属性的时候,有两种情况: - 当前元素已经遍历更新过,并且移除了
src
属性。 - 当前元素并没有
src
属性,意味着不参与懒加载。
不过因为我们总是从 count
开始遍历,因此上面两种情况中, 前者就不存在了,我们只考虑后者。这个时候我们应该选择跳过, 并且 count + 1
。
- 除去上面的情况,我们默认剩下的就是正确有效的元素,执行更新算法,并移除元素的
src
属性,count + 1
。
javascript img.removeAttribute('src') img.src = dataSrc count ++
为了避免频繁触发lazyLaod
函数,我们会选择添加节流功能。本章节使用的是loadash
,当然也可以自己实现一个。
效果
![a20c870135685fe94a9a3bc1f1135dd2.png](https://i-blog.csdnimg.cn/blog_migrate/5eece6479c06c05e103e11c68d557056.png)
基于 getBoundingClientRect
实现懒加载
原理
使用 getBoundingClientRect
的原理和基于 offsetTop
+ clientHeight
+ scrollTop
的原理大同小异。主要不同的点在于,我们使用的是 getBoundingClientRect
方法来返回一个元素的位置信息,而并非上面例子中使用的滚动距离、可视区域高度以及偏移距离来进行计算。
getBoundingClientRect
:返回一个元素的大小以及相对于视图窗口的位置,位置信息是相对于视图窗口的左上角来计算的。
![132298308399099aabefc35142447c6e.png](https://i-blog.csdnimg.cn/blog_migrate/434508ca198e681fcc991892c0552f88.png)
图片来源: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('src')
// 获取相对于视图窗口顶部的距离
const { top } = img.getBoundingClientRect()
// 如果大于可视区域高度,说明在视图窗口之下,跳出
if (top > document.documentElement.clientHeight) {
break
}
// 不参与懒加载的元素, count + 1,进入下一个循环
if (!dataSrc) {
count ++
continue
}
img.removeAttribute('src')
img.src = dataSrc
count ++
}
}
lazyLaodWhtiThrottle = _.throttle(lazyLaod, 200)
window.addEventListener('scroll', lazyLaodWhtiThrottle)
lazyLaod()
代码解析
本例基本的逻辑和上一个方案几乎相同,都需要一个计算步骤来判断是否在视图之内。不同的是,本例中直接使用 getBoundingClientRect()
返回的位置信息,逻辑上更加的清晰。
const { top } = img.getBoundingClientRect()
// 如果大于可视区域高度,说明在视图窗口之下,跳出
if (top > document.documentElement.clientHeight) {
break
}
事实上只需要 top
小于 document.documentElement.clientHeight
,我们就可以认为元素正在可视区域内,并执行更新逻辑。
除此之外,其他逻辑大抵和上一个方案类似,具体可以参考上一个方案。
效果
![252c5407779dedbf4cab0d461adc8959.png](https://i-blog.csdnimg.cn/blog_migrate/2ecf9246bd0a5978d9cdeaec3d257ef5.png)
基于 Intersection Observer API
实现懒加载
Intersection Observer API
首先引用 MDN
上的一句描述:
Intersection Observer API
提供了一种异步观察目标元素与祖先元素或顶级文档 viewport 的交集中的变化的方法。
一直以来,前端或多或少存在着一些视图窗口界内的检测以及图片懒加载的需求。长期以往,我们会使用前面两种位置计算类型的方式来实现我们想要的效果。
然而,我们的代码一般都会在主线程上运行,计算类型的方案显得不那么可靠,或多或少都会拉低整个网站的性能,造成一些不好的体验。尤其是在日益繁杂的功能面前,涉及到很多的广告、图片,可能每一个都有自己独立的交集检测,持续的滚动可能都会造成不可避免的方法调用,这在性能上多少会有影响,即便是采用各种优化手段达到一个相对较好的结果,代码逻辑也往往变得更为复杂。这个时候, Intersection Observer API
就应运而生了。
Intersection Observer API
会注册一个回调函数,当元素和另外一个元素(或者浏览器窗口)产生交集变化的时候,回调函数就会被执行。那么,回到我们的图片懒加载,我们完全可以使用这种方法来代替位置计算,提升网站的性能和体验。
tips:
Intersection Observer API
尚未被浏览器全部支持,使用的时候需要注意浏览器的支持情况。
原理
使用 Intersection Observer API
实现图片懒加载,可以分为几步走:
- 实例化一个
IntersectionObserver
对象observer
,注册回调函数用于处理结果集。 for
循环,观察所有的img
元素。- 回调函数中,每次触发回调传入结果集,遍历判断
isIntersecting
属性,为true
则将src
赋值给src
进行更新,并且在更新之后,通过observer.unobserve
注销对该元素的观察。
代码实现
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('src')
if (!currentChange.isIntersecting) continue
if (!dataSrc) {
observer.unobserve(target)
continue
}
target.src = dataSrc
target.removeAttribute('src')
observer.unobserve(target)
}
});
for (let i = 0; i < imgs.length; i ++) {
observer.observe(imgs[i])
}
}
lazyLaod()
代码解析
这里没有多么复杂的逻辑,主要的逻辑只有三个。
- 实例化一个
IntersectionObserver
实例,注册回调函数。
const observer = new IntersectionObserver(res => {...})
for
循环遍历所有图片,进行观察:
for (let i = 0; i < imgs.length; i ++) {
observer.observe(imgs[i])
}
- 针对回调的结果集做判断和处理,最后记得要注销改元素的观察:
for (let i = 0; i < res.length; i ++) {
const currentChange = res[i]
const { target } = currentChange
const dataSrc = target.getAttribute('src')
if (!currentChange.isIntersecting) continue
if (!dataSrc) {
observer.unobserve(target) // 注销观察
continue
}
target.src = dataSrc
target.removeAttribute('src')
observer.unobserve(target) // 注销观察
}
效果
![60f3d952b275cb71269704704a292ec1.png](https://i-blog.csdnimg.cn/blog_migrate/6de5b314612f9342d3dbbbd14fd16eed.png)
基于 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
和 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('src')
if (!dataSrc) continue
img.loading = 'lazy'
img.src = dataSrc
img.removeAttribute('src')
}
}
}
lazyLaod()
代码解析
这里没什么好说的。实现上十分简单。在搭配 js
的实现中,只需要判断是否支持loading
属性即可,如果支持,则对所有的 img
进行一些操作:
- 添加
loading = "lazy"
- 将
src
赋值给src
效果
![d0b690647c618d95477f76db38ad2531.png](https://i-blog.csdnimg.cn/blog_migrate/d23fd8659fddad0cdce4d48a62c235c3.png)
方案降级
针对以上 4
种方案,在性能体验以及兼容性上各有利弊。或许我们惊喜于原生支持的方案,免去了很多繁杂写法和拥有更优的性能体验,但兼容性却不容乐观,相反,以往看似不过太靠谱的方案在兼容性方面却显得不错。如果我们的程序希望优先选择性能最佳的懒加载方式,完全可以通过策略模式,在当前浏览器环境使用中判断属性支持程度以动态切换懒加载策略,在支持度不高的浏览器中也能降级为兼容性方案。
当然,本文不会去深入实现这个东西。因为这不是这篇文章的重点。相关的功能我会在之后找个时间去实现,希望也能总结出一个博文。
总结
本文细数了 4
种前端图片懒加载的实现方案,其中,位置计算类型的共通思想都是计算元素相对于可视区域的位置,只不过基于offsetTop
+ clientHeight
+ scrollTop
的方案中,位置的计算借助了元素的偏移距离、文档的滚动距离以及可视区域高度,而基于 getBoundingClientRect
的方案则是通过这个 API
来获取相对于视口左上角的位置。两者相比,后者更加清晰、简单。
而 Intersection Observer API
的方案,避免了前两种方案位置计算的步骤,只在交集检测的结果集进行操作,当交集变化的时候,回调函数将会被调用,通过判断结果集 isIntersecting
属性即可。
最后一个方案,则是基于 img
标签原生支持的功能,通过设置 loading="lazy"
来实现图片懒加载,不需要借助 js
计算,仅仅依靠原生支持以及浏览器优化,性能更佳。但是兼容性方面目前来讲非常糟糕。
参考文献
Intersection Observer
浏览器IMG图片原生懒加载loading=”lazy”实践指南
Element.getBoundingClientRect()
图片懒加载
代码仓库
https://github.com/chenhaoren/pra--front-end/tree/master/%E6%87%92%E5%8A%A0%E8%BD%BD
在线直通车
本文首发于:老生常谈之图片懒加载