图片懒加载实现

图片懒加载

什么是懒加载

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

如何实现懒加载

我们知道,对于图片而言,我们只要设置了其 src 属性,它就可以自动载入图片。因此,图片的懒加载,其实就是让设置 src 属性这个行为延迟执行。例如,当一张图片出现在用户的视野当中的时候,我们再去设置其 src 属性,这样就可以达到延迟加载的目的。显然,首次需要加载的图片数量越少,首屏渲染时间就会越短。基于这种朴实无华的思路,这里我们介绍三种实现延迟加载的方案。

监听滚动事件

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

示意图

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

let lazyLoadByDefault = function(imgs) {
    var H = document.documentElement.clientHeight;
     // 基于各浏览器下 scrollTop 的差异。
    var S = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
    for (var i = 0; i < imgs.length; i++) {
        if (H + S > getTop(imgs[i]) && 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);
}

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

let getTop = function(e) {
    var T = e.offsetTop;

    // offsetParent:距离该元素最近的、有定位属性(position 不等于 static)的父级元素。
    while(e = e.offsetParent) {
        T += e.offsetTop;
    }
    return T;
}

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

<img src="./imgs/loading.gif" data-src="./imgs/1.jpg" data-loading="lazy" alt="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 && 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");
            }
        }
    }
}

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

IntersectionObserver

除了使用上面的手工计算的方式,我们还可以使用 IntersectionObserver 这个 API。关于这个 API,官方是这样描述的: IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法。祖先元素与视窗被称为**根(root)。大意就是说,这个 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");
                // 使 IntersectionObserver 停止监听特定目标元素。
                lazyImageObserver.unobserve(lazyImage);
            }
          }
        });
      });

      lazyImages.forEach(function(lazyImage) {
        // 使 IntersectionObserver 开始监听一个目标元素。
        lazyImageObserver.observe(lazyImage);
      });
}

var imgs = document.querySelectorAll('img');
lazyLoadByObserver(imgs);

可以注意到,我们会尝试去观察每一个 Img 元素,当它和可视视口交叉(重叠)时,表明它正位于可视视口内,此时,我们会从 data-src 属性上读取图片的地址,然后将其赋值给 src 属性。这样,我们就实现了图片的懒加载。考虑到第一种方案,你可能会因为搞不清楚那些数值而导致计算上的错误,这个方案就相对简单一点。当然,你真正应该考虑的是,它的兼容性如何:除了寿终正寝的 IE 浏览器,一腔孤勇的 Safari 浏览器,这个 API 的兼容性还是挺不错的。如果你依然对这一点感到如履薄冰,可以使用下面的代码来进行兼容性判断:

var imgs = document.querySelectorAll('img');
if ("IntersectionObserver" in window) {
    lazyLoadByObserver(imgs)
} else {
    lazyLoadByDefault(imgs)
}

混合使用上面两种方案是一种不错的选择。

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

浏览器原生方案

从 Chrome 77、Firefox 75 及其以上版本开始,浏览器开始支持图片和框架的原生懒加载特性。这意味着,从这一刻开始,我们有了浏览器级别的原生懒加载方案,即:在 <img> 或者 <iframe> 标签上添加 loading = 'lazy' 这组属性即可,下面是一个非常朴素的示例:

<img src="./example.jpg" loading="lazy" alt="this is a example for image lazy loading.">

由于是浏览器级别的懒加载方案,所以,它不需要我们再像上面两种方案一样,编写额外的 JavaScript 代码。loading 属性有下面两个取值:

  • lazy:图片或者框架使用懒加载,即元素即将可见的时候加载,且浏览器内部会定义一个元素和可视视口的距离的阈值,越接近该值表明元素越可见。
  • eager:立即加载图像或者框架,无论元素是否在可视视口内可见。

这个方案应该是三种方案中最简单的,可实际中还是多多少少有一些问题,譬如没有办法给定一个占位图片、每次加载图片的数量与屏幕高度、网速、窗口尺寸等因素有关,甚至连加载图片的顺序都是不确定的,这就意味着这中间有很多难以控制的因素,如果考虑到兼容性或者 Polyfill 的问题,这个方案就显得没那么有吸引力了。在测试的过程中会发现,有时候它是一次性就把多张图片加载出来了。所以,我个人的观点是,想偷懒的话可以直接用这个特性,但如果要控制懒加载过程中的细节,这个特性就不太合适了。

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

本文小结

本文主要分享了前端图片懒加载的三种实现思路,即监听滚动事件、IntersectionObserver 以及浏览器原生支持的 loading = 'lazy'。懒加载的基本思路是延迟加载,对图片而言,我们更希望它可以在即将出现在用户视野中的时候去加载,因为这样能减少不必要的资源请求,同时可以缩短首屏渲染时间。因此,图片的懒加载是前端性能优化过程中不可或缺的一种优化策略。判断一个图片是否位于可视视口内,可以采用手工计算的方式,当然这里更推荐使用 IntersectionObserver 这个 API。 loading = 'lazy' 是一种浏览器级别的懒加载的特性,虽然它的用法非常简单,可是考虑到整个懒加载的过程,对用户而言完全就是个黑箱,因此,如果你想更精确地控制懒加载的细节,譬如给定一个占位图片,这种情况下该方案就不太合适。更不必说,它里面有很多不确定的或者难以控制的因素。

END

文章取自「云来雁去」的原创文章:聊一聊前端图片懒加载背后的故事原文,这边增加了部分注释并且调整了部分内容。
文章参考:深入理解定位父级 offsetParent 及偏移大小等。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值