为了账号安全,请及时绑定邮箱和手机立即绑定

老生常谈之图片懒加载

标签:
Html5 JavaScript

前言

当我们在导航栏上输入我们性感的 url 并按下回车的时候,浏览器其实做了非常多的工作,撇去复杂细节不讲,直观上就包括了对资源的请求和下载,以及视图的渲染和呈现。假设我们有一个多图文的新闻资讯类的网站,在网络请求的时候,想要一次性获取所有的图片并加载展示出来,那体验肯定是非常糟糕的。为什么?

首先我们来看看一个图片在 HTML 中是如何表现的:

 <img src="https://xxxx/xxx.png"/>

这其中对于图片资源的引用,我们用到了属性 src

src , 实际上是 sourc 的简称,主要用于替换当前内容。当浏览器执行到 src 的时候,就会暂停其他资源的下载和处理,直到当前的资源加载和执行完毕,这也就是为什么我们的 script 标签尽量应该放在底部的原因。

因此,回到我们上面多图文新闻资讯类网站的假设上,解决多图文呈现的性能和体验问题,最常用的方法就是对资源的下载和处理做一些限制,既图片懒加载

什么是图片懒加载?

图片懒加载,顾名思义,就是懒。那么懒到何种程度?简单来说,就是你翻不到的,我都可能懒得去给你请求,更谈不上渲染呈现给你看了。这样子做的好处,一方面我们避免了一时间加载过多图片造成的网络带宽压力,另一方面也让前端界面加载更快,体验更佳。

基本思想

初始化

img 标签的 src 属性上指向默认占位图,data-src 属性则指向真正图片的 url

视图 scroll 或者 resize

将视图窗口内的所有 img 标签的 data-src 赋值给 src,完成当前图片的加载。

结束

所有的图片都加载完毕,移除 scroll / resize 事件,或者移除相关懒加载监控事件。

图片懒加载实现方案

关于图片懒加载的实现方案,我将其分为三类,分别是视口位置计算交集观察以及 HTML 原生支持

其中, 视口位置计算方案有两个,分别是:

  1. 基于 offsetTop + clientHeight + scrollTop 实现懒加载
  2. 基于 getBoundingClientRect 实现懒加载

交集观察法:

  1. 基于 Intersection Observer API 实现懒加载

HTML 原生支持:

  1. 基于 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
    // 遍历超出视图底部的元素则跳出循环,没有必要再找下去
    // 如果图片没有 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() // 初始

代码解析

对照上面代码来讲:

  1. count , 一个计数器,代表当前更新了 imgs 数组中多少个元素了。在我们的懒加载中,也会当做下一个要更新元素的下标传给 i

     for (let i = count; i < imgs.length; i ++){ // ... }
    

    其次,当 count 大于或者等于 imgs 的长度之后,说明所有元素都已经遍历过,则应该清除滚动事件。

  2. 在遍历元素上除了基于 count 的第一个元素下标的优化,也应该停止视图之下(可见区域后面)的元素的遍历,因为剩下的元素都是在视图之外,我们并不需要更新。

    if (img.offsetTop > scrollTop + viewHeight) {
        break
    }
    
  3. 当遍历的元素没有 data-src 属性的时候,有两种情况:

    1. 当前元素已经遍历更新过,并且移除了data-src 属性。
    2. 当前元素并没有 data-src 属性,意味着不参与懒加载。

    不过因为我们总是从 count 开始遍历,因此上面两种情况中, 前者就不存在了,我们只考虑后者。这个时候我们应该选择跳过, 并且 count + 1

  4. 除去上面的情况,我们默认剩下的就是正确有效的元素,执行更新算法,并移除元素的 data-src 属性, count + 1

    img.removeAttribute('data-src')
    img.src = dataSrc
    count ++
    

为了避免频繁触发 lazyLaod 函数,我们会选择添加节流功能。本章节使用的是 loadash,当然也可以自己实现一个。

效果

基于 getBoundingClientRect 实现懒加载

原理

使用 getBoundingClientRect 的原理和基于 offsetTop + clientHeight + scrollTop 的原理大同小异。主要不同的点在于,我们使用的是 getBoundingClientRect 方法来返回一个元素的位置信息,而并非上面例子中使用的滚动距离、可视区域高度以及偏移距离来进行计算。

getBoundingClientRect:返回一个元素的大小以及相对于视图窗口的位置,位置信息是相对于视图窗口的左上角来计算的。

DOMRect 示例图

代码实现

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()

代码解析

本例基本的逻辑和上一个方案几乎相同,都需要一个计算步骤来判断是否在视图之内。不同的是,本例中直接使用 getBoundingClientRect() 返回的位置信息,逻辑上更加的清晰。

const { top } = img.getBoundingClientRect()

// 如果大于可视区域高度,说明在视图窗口之下,跳出
if (top > document.documentElement.clientHeight) {
    break
}

事实上只需要 top 小于 document.documentElement.clientHeight,我们就可以认为元素正在可视区域内,并执行更新逻辑。

除此之外,其他逻辑大抵和上一个方案类似,具体可以参考上一个方案。

效果

基于 Intersection Observer API 实现懒加载

Intersection Observer API

首先引用 MDN 上的一句描述:

Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档 viewport 的交集中的变化的方法。

一直以来,前端或多或少存在着一些视图窗口界内的检测以及图片懒加载的需求。长期以往,我们会使用前面两种位置计算类型的方式来实现我们想要的效果。

然而,我们的代码一般都会在主线程上运行,计算类型的方案显得不那么可靠,或多或少都会拉低整个网站的性能,造成一些不好的体验。尤其是在日益繁杂的功能面前,涉及到很多的广告、图片,可能每一个都有自己独立的交集检测,持续的滚动可能都会造成不可避免的方法调用,这在性能上多少会有影响,即便是采用各种优化手段达到一个相对较好的结果,代码逻辑也往往变得更为复杂。这个时候, Intersection Observer API 就应运而生了。

Intersection Observer API 会注册一个回调函数,当元素和另外一个元素(或者浏览器窗口)产生交集变化的时候,回调函数就会被执行。那么,回到我们的图片懒加载,我们完全可以使用这种方法来代替位置计算,提升网站的性能和体验。

tipsIntersection Observer API 尚未被浏览器全部支持,使用的时候需要注意浏览器的支持情况。

原理

使用 Intersection Observer API 实现图片懒加载,可以分为几步走:

  1. 实例化一个 IntersectionObserver 对象 observer,注册回调函数用于处理结果集。
  2. for 循环,观察所有的 img元素。
  3. 回调函数中,每次触发回调传入结果集,遍历判断 isIntersecting 属性,为 true 则将 data-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('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()

代码解析

这里没有多么复杂的逻辑,主要的逻辑只有三个。

  1. 实例化一个 IntersectionObserver 实例,注册回调函数。

     const observer = new IntersectionObserver(res => {...})
    
  2. for 循环遍历所有图片,进行观察:

    for (let i = 0; i < imgs.length; i ++) {
        observer.observe(imgs[i])
    }
    
  3. 针对回调的结果集做判断和处理,最后记得要注销改元素的观察:

    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) // 注销观察
    }
    

效果

基于 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 标签赋予 srcdata-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()

代码解析

这里没什么好说的。实现上十分简单。在搭配 js 的实现中,只需要判断是否支持loading 属性即可,如果支持,则对所有的 img 进行一些操作:

  1. 添加 loading = "lazy"
  2. data-src 赋值给 src

效果

方案降级

针对以上 4 种方案,在性能体验以及兼容性上各有利弊。或许我们惊喜于原生支持的方案,免去了很多繁杂写法和拥有更优的性能体验,但兼容性却不容乐观,相反,以往看似不过太靠谱的方案在兼容性方面却显得不错。如果我们的程序希望优先选择性能最佳的懒加载方式,完全可以通过策略模式,在当前浏览器环境使用中判断属性支持程度以动态切换懒加载策略,在支持度不高的浏览器中也能降级为兼容性方案。

当然,本文不会去深入实现这个东西。因为这不是这篇文章的重点。相关的功能我会在之后找个时间去实现,希望也能总结出一个博文。

总结

本文细数了 4 种前端图片懒加载的实现方案,其中,位置计算类型的共通思想都是计算元素相对于可视区域的位置,只不过基于offsetTop + clientHeight + scrollTop 的方案中,位置的计算借助了元素的偏移距离、文档的滚动距离以及可视区域高度,而基于 getBoundingClientRect 的方案则是通过这个 API来获取相对于视口左上角的位置。两者相比,后者更加清晰、简单。

Intersection Observer API 的方案,避免了前两种方案位置计算的步骤,只在交集检测的结果集进行操作,当交集变化的时候,回调函数将会被调用,通过判断结果集 isIntersecting 属性即可。

最后一个方案,则是基于 img 标签原生支持的功能,通过设置 loading="lazy" 来实现图片懒加载,不需要借助 js 计算,仅仅依靠原生支持以及浏览器优化,性能更佳。但是兼容性方面目前来讲非常糟糕。

参考文献

代码仓库

在线直通车

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1
获赞与收藏
13

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消