• 聊一聊前端图片懒加载背后的故事


    相信大家已经注意到我博客有了一点变化,因为博主最近利用空闲时间对博客进行了优化。经过博主的不懈努力,首屏渲染时间从原来的 2.0 秒缩短到了 1.7 秒。虽然这个优化相当得感人,不过我还是在这个过程中有所收获。Stack 这个主题中大量使用了图片这种元素,特别是首页中那些作为文章封面而存在的图片。我原本是打算借鉴一下 Wincer 这位网友的博客样式,可是考虑到选择封面、图片尺寸…等等的因素,我最终还是决定写一个相对“平庸”的布局样式,即你现在看到的这个版本,本次优化的重点主要在于使用 CDN 加速、对图片进行压缩、编译期生成缩略图、使用懒加载这些常见的策略。在今天这篇博客中,我们来重点聊一聊前端图片的懒加载,希望能为大家带来一点新的启发或者思考。

    什么是懒加载

    懒加载,即:LazyLoad,其核心全在于“懒”这个字眼上。虽然,这个字在生活中更多的是表示一种贬义,可正如气体有活性和惰性的区别,这里我们将其理解为延迟加载,或许会更合适一点,因为生活早已告诉我们,只要你打算偷懒,就一定会造成拖延。因此,懒加载其实就是一种通过延迟加载对网页性能进行优化的方法。一个典型的例子是,当网页中有滚动条的时候。此时,网页的一部分区域对于浏览器视窗而言是不可见的。如果将一次性将其加载出来,这其实是一种资源的浪费,因为你不确定用户是否有耐心浏览完整个网页。在对网页的浏览量进行评估的时候,通常都会有一个跳出率的概念。可想而知,用户更容易被网页上的超链接吸引,在不同的网页间跳转。退一步讲,如果一个网页上有非常多的图片,等待这些图片全部加载完会浪费大量时间,进而影响到用户体验。博主原本就是为了减少首屏渲染时间,所以,不管从哪一个角度来看,懒加载或者说延迟加载,对于前端的性能优化都有着极其重要的意义,而这正是博主写作这篇文章的原始动机所在。

    在这里插入图片描述

    如何实现懒加载

    我们知道,对于图片而言,我们只要设置了其 src 属性,它就可以自动载入图片。因此,图片的懒加载,其实就是让设置 src 属性这个行为延迟执行,譬如,当一张图片出现在用户的视野当中的时候,我们再去设置其 src 属性,这样就可以达到延迟加载的目的。显然,首次需要加载的图片数量越少,首屏渲染时间就会越短,这不正是我们想要达到的目的吗?基于这种朴实无华的思路,这里我们介绍三种实现延迟加载的方案,如果大家还有更好的方案,欢迎大家在评论区补充或者讨论。

    监听滚动事件

    首先,我们最容易想到的一种思路是,监听网页的滚动事件,因为我们更希望看到的结果是,当元素滚动到可视视口内的时候再去加载。此时,问题的关键是如何判断当前元素在可视视口内,在解决这个问题之前,我们先来看看下面这幅图片,它展示了网页中的 clientHeightscrollTop 以及 offsetTop 这三个数值间的关系:

    在这里插入图片描述

    可以注意到,当 clientHeight(H) + scrollTop(S) > offsetTop 的时候,即表示当前元素位于可视视口内。基于这个思路,我们可以编写出下面的代码:

    let lazyLoadByDefault = function(imgs) {
        var H = document.documentElement.clientHeight;
        var S = document.documentElement.scrollTop || document.body.scrollTop;
        for (var i = 0; i < imgs.length; i++) {
            if (H + S > getTop(imgs[i])) {
                if (imgs[i].getAttribute('data-loading') == 'lazy' && 
                    imgs[i].getAttribute('data-src')) 
                {
                    let src = decodeURI(imgs[i].getAttribute('data-src'))
                    imgs[i].src = src
                    imgs[i].removeAttribute("data-loading")
                }
            }
        }
    }
    
    window.onload = window.onscroll = function() {
        var imgs = document.querySelectorAll('img');
        lazyLoadByDefault(imgs)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其中,getTop() 方法用于计算 offsetTop,为什么不直接使用这个值呢,因为这个值是相对于父元素而言的,所以,考虑到元素嵌套的问题,我们必须要计算出每一个层级相对于父级的 offsetTop,然后再将它们累加起来。除此之外,我们给每个 Img 元素增加了一个自定义属性 data-src,它里面放置的就是真正的图片地址,我们只要在合适的时机将其赋值给 src 即可。当然,为了效果更好一点,你可以准备一张表示 loading 的图片放在 src 属性上:

    let getTop = function(e) {
        var T = e.offsetTop;
        
        while(e = e.offsetParent) {
            console.log(e)
            T += e.offsetTop;
        }
        return T;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    相应地,此时我们需要像下面这样来准备 HTML 结构:

    <img src="./imgs/loading.gif" data-src="./imgs/1.jpg" data-loading="lazy" alt="1" />
    
    • 1

    考虑到滚动事件可以引起图片的重复加载,这里采用的方案是:增加一个一个自定义属性 data-loading,并在加载完后移除该属性。定义这样一个属性的原因,一定程度上是为了避免和 loading='lazy' 撞车,而关于这个特性,我们会在下面的内容中做进一步的讲解。在这里,如果你对于 clientHeightscrollTop 以及 offsetTop 这三个数值一脸懵逼的话,我们还有一种稍微简单一点的做法,即调用 getBoundingClientRect() 这个方法,它会返回当前元素相对于可视视口的位置信息。此时,只要满足 top < clientHieght 这个条件即可。因此,上面的代码可以进一步简化为:

    let lazyLoadByDefault = function(imgs) {
        var H = document.documentElement.clientHeight;
        for (var i = 0; i < imgs.length; i++) {
            var bounding = imgs[i].getBoundingClientRect()
            if (bounding.top <= H) {
                if (imgs[i].getAttribute('data-loading') == 'lazy' && 
                    imgs[i].getAttribute('data-src')) 
                {
                    let src = decodeURI(imgs[i].getAttribute('data-src'))
                    imgs[i].src = src
                    imgs[i].removeAttribute("data-loading")
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    下面是博主编写的一个简单示例,仅供大家参考:

    在这里插入图片描述
    参考示例:https://codepen.io/qinyuanpei/pen/wvmmyzZ

    IntersectionObserver

    除了使用上面的手工计算的方式,我们还可以使用 IntersectionObserver 这个 API。关于这个 API,官方是这样描述的: IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法。祖先元素与视窗被称为根。我们尝试将其翻译成人话,大意就是说,这个 API 可以判断目标元素是否与顶级文档视窗交叉(重叠),两者重叠其实就是说目标元素在可视视口内。一旦理解了这一点,我们就可以轻而易举地写出下面的代码:

    let lazyLoadByObserver = function(lazyImages) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
              if (entry.isIntersecting) {
                let lazyImage = entry.target;
                if (lazyImage.getAttribute('data-loading') == 'lazy' && 
                    lazyImage.getAttribute('data-src')) 
                {
                    let src = decodeURI(lazyImage.getAttribute('data-src'))
                    lazyImage.src = src
                    lazyImage.removeAttribute("data-loading")
                    lazyImageObserver.unobserve(lazyImage);
                }
              }
            });
          });
      
          lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
          });
    }
    
    var imgs = document.querySelectorAll('img');
    lazyLoadByObserver(imgs);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可以注意到,我们会尝试去观察每一个 Img 元素,当它和可视视口交叉(重叠)时,表明它正位于可视视口内,此时,我们会从 data-src属性上读取图片的地址,然后将其赋值给 src 属性。这样,我们就实现了图片的懒加载。我个人认为,这个 API 非常好用,考虑到第一种方案,你可能会因为搞不清楚那些数值而导致计算上的错误,这个方案就相对简单一点。当然,你真正应该考虑的是,它的兼容性如何:

    在这里插入图片描述

    从这张图中可以看出,除了寿终正寝的 IE 浏览器,一腔孤勇的 Safari 浏览器,这个 API 的兼容性还是挺不错的。如果你依然对这一点感到如履薄冰,可以使用下面的代码来进行兼容性判断:

    var imgs = document.querySelectorAll('img');
    if ("IntersectionObserver" in window) {
        lazyLoadByObserver(imgs)
    } else {
        lazyLoadByDefault(imgs)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    相信你已经猜到,博主的博客其实是混合着使用了上面两种方案,在使用懒加载以后,首页打开的时候将不会再加载所有封面图片,而是等到这些封面图片出现在可视视口内的时候再去加载,正是这一点点微不足道的工作,让博客的首屏渲染时间缩短了 0.3 秒,我想说,这实在有趣!

    在这里插入图片描述

    参考示例:https://codepen.io/qinyuanpei/pen/JjLLpdJ

    浏览器原生方案

    在这里插入图片描述

    Chrome 77Firefox 75 及其以上版本开始,浏览器开始支持图片和框架的原生懒加载特性。这意味着,从这一刻开始,我们有了浏览器级别的原生懒加载方案,即:在 或者