加载优化(图像延迟加载)

图像延迟加载

前言:

​ 相要得到更好的性能体验,只靠资源压缩与恰当的文件格式选型,是很难满足期望的。我们还需要针对资源加载过程进行优化,该环节所要做的内容可概括为分清资源加载的优先级顺序,仅加载当前所必需的资源,并利用系统空闲提前加载可能会用利的资源。这便是本章将要探讨的内容:资源的优先级、延迟加载和预加载。

​ 什么是图像的延迟加载,如何高效地实现延迟加载。随着近些年视频资源越来越多的使用,也会捎带介绍视频资源的延迟加载。然后谈谈浏览器对于资源优先级的划分和控制,既然可以通过将非关键资源延迟加载来提升性能,那么是否可以利用系统使用的空闲,预先去加载可能会使用到的资源。

​ 本节介绍什么是延迟加载,以及这种优化策略产生的逻辑和实现原理。笔者认为只有先理解了一种原理或方法的缘起流变,才能知道怎样的实现方式是更高效的、更贴近业务场景的。

一、什么是延迟加载

​ 首先来想象一个场景,当浏览一个内容丰富的网站时,比如电商的商品列表页、主流视频网站的节目列表等,由于屏幕尺寸的限制,每次只能查看到视窗中的那部分内容,而要浏览完页面所包含的全部信息,就需要滚动页面,让屏幕视窗依次展示出个页面的所有局部内容。

​ 显而易见,对于首屏之外的内容,特别是图片和视频,一方面 由于资源文件很大,若是全部加载完,既费时又费力,还容易阻塞渲染引起卡顿:另一方面,就算加载完成,用户也不一定会滚动屏幕浏览到全部页面内容,如果首屏内容没能吸引住用户,那么很可能整个页面就将遭到关闭。

​ 既然如此,本着节约不浪费的原则,在首次打开网站时,应尽量只加载首屏内容所包含的资源,而首屏之外涉及的图片或视频,可以等到用户滚动视窗浏览时再去加载。

​ 以上就是延迟加载优化策略的产生逻辑,通过延迟加载“非关键”的图片及视频资源,使得页面内容更快地呈现在用户面前。这里的“非关键”资源指的就是首屏之外的图片或视频资源,相较于文本、脚本等其他资源来说,图片的资源大小不容小觑。这个优化策略在业界已经被广泛使用,接下来笔者就以天猫购物网站的商品列表页为例,具体看看延迟加载是如何实现的,如图所示。

​ 图左侧是手机端常见的电商购物平台的商品列表页,右侧是其对应的DOM树结构。其中在区域上方,整齐如排比句般的

结构,所对应的正是列表页中一行行的商品项。以其中一件商品为例, 展开它的DOM树,直到找到展示该商品图片的标签。为了方便说明,笔者将这个标签的相关细节摘录如下:

<img class="boom-item-item" autowebp="false" autopixelratio="true" forceupdate="true" data-bindkey="pic" data-itemid="700023719087" data-size="348x348" data-rewrite="{size:'348x348'}" data-lazy-type="img" data-lazy-id="lazyId-11" data-lazy-manager-id="gLazyM-1" data-in-view-range="1" src="//gw.alicdn.com/bao/uploaded/i3/3058655500/O1CN01CyNBBW1qV3AjEusqC_!!0-item_pic.jpg_360x360q75.jpg_.webp">

​ 这里主要关注其中的src属性,src 属性代表了一个CDN上的图片资源。要知道当标签的src属性被赋予了一一个URL后,它就会立刻向该URL发起资源请求。所以这个商品的标签代表的就是一个商品图片的占位符。

​ 接下来我们找到一个位于屏幕视窗外,还未加载的商品图片和已加载的图片,相比较看看二者标签上的属性值有何不同。首先保持左侧页面显示窗口不发生滚动,在DevTools工具的Elements页签下,寻找还未呈现在左侧视窗中的商品项,容易找到它的DOM结构。

​ 首先,我们依然关注标签的src属性,这里并不是图片资源的外链URL,取而代之的是一个在图像优化章节中介绍过的Base64图片,与外链URL不同的是,Base64图片已经包含了图片的完全编码,可以直接拿来渲染,而无须发起任何网络请求。

​ 这意味着该Base64图片仅仅是在真实图片显示出来前用以占位的,同时注意到所有未展示在页面视窗中的商品,其图片占位src属性值均使用了相同的Base64的值.当页面发生滚动时,之前未出现在视窗中的商品出现在视窗中后,其商品图片的真实URL会被替换到标签的src属性上,进而发起资源请求。

​ 我们知道了什么是延迟加载。以及为什么要使用延迟加载,并通过观察一个商品列表页的案例,基本清楚了延迟加载的处理过程,接下来将通过三种方法来具体实现延迟加载。

二、实现图片的延迟加载:传统方式

​ 就是事件监听的方式,通过监听scroll事件与resize 事件,并在事件的回调函费中去判断,需要进行延迟加载的图片是否进入视窗区域。

​ 首先根据前面的例子,定义出将要实现延迟加载的标签结构:

我们只需要关注三个属性。

  • class 属性,稍后会在JavaScript中使用类选择器选取需要延迟加载处理的标签。
  • src属性,加载前的占位符图片,可用Base64图片或低分辨率的图片。
  • data-src属性,通过该自定义属性保存图片真实的URL外链。

假设以三张图片为例进行延迟加载的标签列表如下:

<img class="lazy" src-"data: image/gif;base64, iVBORwOKGg.. .BJRUErkJgqs-.data-src="https://res.cloudinary.com/ .../tacos-2x.jpg"width="385" height="108" alt="Some tacos. ">

<img class="lazy" src="data:image/gif;base64, iVBORw0KGg. . . BJRU5ErkJggg==" data-src="https:// res.cloudinary.com/d. . ./modem-2x . png" width="320" height="176" alt="A 56k modem. ">

<img class="lazy" src="data:image/gif;base64, iVBORw0KGg.. . BJRU5ErkJggg==" data-src="https ://res. cloudinary. com/ ../st-paul-2x. jpg" width="400" height="267" alt="A city skyline. ">

​ 具体的JvsSerpr实现逻辑如下,在文档的DOMContentLoaded事件中,添加延迟加载处理逻辑,首先获取class属性名为lazy的所有标签,将这些标签看存在一个名为lazylmages 的数组中,表示需要进行延迟加载但还未加载的图片集合。当一个图片被加载后,便将其从lazylmages数组中移除,直到lazyImages数组为空时,表示所有待延迟加载的图片均已经加载完成,此时便可将页面滚动事件移除。

​ 接下来的关键就是判断图片是否出现在视窗中,这里使用了getBoundingClientRect()函数获取元素的相对位置,如图所示。它会返回图片元素的宽width和高height,及其与视窗的相对位置:元素上边缘与屏幕视窗顶部之间的距离top,元素左边缘和屏幕视窗左侧之间的距离left,元素下边缘和屏幕视窗顶部之间的距离bottom 以及元素右边缘和屏幕视窗左侧之间的距离right, 其具体含义可参考示意图,window.innerHeight 表示整个视窗的高度。

getBoundingClientRect()函数获取元素的相对位置

​ 对于只可上下滚动的页面,判断一个图片 元素是否出现在屏幕视窗中的方法其实显而易见,即当元素上边缘距屏幕视窗顶部的top 值小于整个视窗的高度window.innerHeight时,预加载的事件处理代码如下:

document.addEventListener("DOMContentLoaded", function () {
  //获取所有需要延迟加载的图片
  let lazyImages = [].sllce.call(document.querySelectorAll("img.1azy"));
  //限制函数频繁被调用
  let active = false;
  const lazyLoad = function () {
    if (active === false) {
      active = true;
      setTimeout(function () {
        lazyImages.forEach(function (lazyImage) {
          //判断图片是否出现在视窗中
          if (
            lazyImage.getBoundingClientRect().top <= window.innerHeight &&
            lazyImage.getBoundingClientRect().bottom >= 0 &&
            getComputedstyle(lazyImage).display !== "none"
          ) {
            // 将真实的图片URL赋值给src属性,发起请求加载资源

            lazyImage.src = lazyImage.dataset.src;

            //图片加载完成后,取消监控以防止重复加载

            lazyImage.classList.remove("1azy");

            lazyImages = lazyImages.filter(function (image) {
              return image !== lazyImage;
            });

            //所有延迟加载图片加载完成后,移除事件触发处理函数

            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyload);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });

        active = false;
      }, 200);
    }
  };

  document.addEventListener("scroll", lazyLoad);
  window.addEventListener("resize", lazyLoad);
  window.addEventListener("orientationchange", lazyLoad);
});

​ 由于无法控制用户随心所欲地滑动鼠标滚轮,从而造成scroll 事件被触发地过于频繁,导致过多的冗余计算影响性能。所以此处笔者将延迟加载的处理过程置于一个200ms的异步定时器中,并在每次处理完成后,通过修改标志位active 的方式来对方法的执行进行限流。

​ 即便如此也有潜在的性能问题,因为重复的setTimeout调用是浪费的,虽然进行了触发限制,但当文档滚动或窗口大小调整时,不论图片是否出现在视窗中,每200ms都会运行一次检查,并且跟踪尚未加载的图片数量,以及完全加载完后,取消绑定滚动事件的处理函数等操作都需要开发者来考虑。

​ 如此来看,虽然传统的延迟加载实现方式具有更好的浏览器兼容性,但也存在如上所述不可逾越的性能问题与编码的烦琐性,这便有了下面一种新的实现方式。

三、实现图片的延迟加载: Intersection Observer方式

​ 现代浏览器已大多支持了Intersection Observer API,可以通过它来检查目标元素的可见性,这种方式的性能和效率都比较好。

​ 关于Intersection Observer的概念和用法,可以参考阅读相关文档,这里用一句话简述:每当因页面滚动或窗口尺寸发生变化,使得目标元素(target) 与设备视窗或其他指定元素产生交集时,便会触发通过Intersection Observer API配置的回调函数,在该回调函数中进行延迟加载的逻辑处理,会比传统方式显得更加简洁而高效。

​ 以下便是Intersection Observer 方式的具体实现,此方式仅需创建一个新的Observer,并在类名为lazy的标签进入视窗后触发回调。

document.addEventListener("DOMContentLoaded", function () {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  //判断浏览器兼容性
  if (
    "Intersectionobserver" in window &&
    "IntersectionObserverEntry" in window &&
    "intersectionRatio" in window.Intersection0bserverEntry.prototype
  ) {
    //新建Intersectionobserver对象,并在其回调函数中实现关键加载逻辑
    let lazyImageObserver = new IntersectionObserver(function (
      entries,observer
    ) {
      entries.forEach(function (entry) {
        //判断图片是否出现在视窗中
        if (entry.isIntersecting) {
          let lazyImage = entry.target;

          lazyImage.src = lazyImage.dataset.src;
          //图片加载完成后,取消监控防止重复加载
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });
    lazyImages.forEach(function (lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  }
});

​ 这种方式判断元素是否出现在视窗中更为简单直观,应在实际开发中尽量使用,但其问题是并非所有浏览器都能兼容。具其体的浏览器兼容情况可在站点上进行查看,根据网站用户的硬件分布情况来权衡是否使用,以及使用后是否需要进行兼容处理。在将这种方式引入项目之前,应当确保已做到以下两点。

(1)做好尽量完备浏览器兼容性检查,对于兼容Intersection Observer API的浏览器,采用这种方式进行处理,而对于不兼容的浏览器,则切换回传统的实现方式进行处理。

(2)使用相应兼容的polyfill插件,在W3C官方Git账号下就有提供。除此之外,还有第三种通过Css属性的实现方案。

四、实现图片的延迟加载:CSS类名方式

​ 这种实现方式通过css的 backgound-image 属性来加载图片,与判断标签src属性是否有要请求图片的URL不同,Css中图片加载的行为建立在浏览器对文档分析基础之上。

​ 具体来说,当DOM树、CSSOM树及渲染树生成后,浏览器会去检查CSS以何种方式应用于文档,再决定是否请求外部资源。如果浏览器确定涉及外部资源请求的CSS规则在当前文档中不存在时,便不会去请求该资源。图片列表如下所示:

<div class="wrapper">
<div class="lazy-background one"></div>
<div class="lazy-background two"></div>
<div class="lazy-background three"></div>
</div>

​ 具体的实现方式是通过javascript来判断元素是否出现在视窗中的,当在视窗中出现的时候,为其class属性添加visible类名,而在css文件中,为同一类名元素定义出带 .visible和不带.visible的两种包含 background-image规则。

​ 不带 .visible的图片规则中的background-image属性可以是低分辨辨率的图片或Base64图片,而带.visible的图片规则中的background-image属性为为希望展示的真实图片URL.

​ 具体JavaScript的实现过程如下所示,判断图片元素是否出现在视窗内的逻辑,与上面的Intersection Observer 方式相同。同样为了确保浏览器的兼容性,在实际应用中应确保提供回退方案或polyfill。

五、原生的延迟加载支持

​ 除了上述通过开发者手动实现延迟加载逻辑的方式,从Chrome 75版本开始,已经可以通过 img 和 ifram e标签的loading属性原生支持延迟加载了,loading 属性包含以下三种取值。

  • lazy: 进行延迟加载。
  • eager: 立即加载。
  • auto:浏览器自行决定是否进行延迟加载。

若不指定任何属性值,loading 默认取值auto。

​ 之前讲到延迟加载的独发触发机,都是当目标图像文件经页面滚动出现在屏幕视窗中时,能发对图像资源的请求。但从体验上考虑,这样处理并不完美,因为当图像标签出现在屏幕视窗中时,还只是占位符图像。

​ 如果网络存在延迟或图像资源过大,那么它的请求加载过程是可以被用户感知的。更好的做法是在图像即将滚动出现在屏幕视窗之前一段距离, 就开始请求加载图像或iframe中的内容,这样能很好地缩短用户的等待加载时长。

​ 兼容性处理:通过使用新技术优化了延迟加载的实现方式,同时也应当注意新技本在不同览器之间的兼容性,在使用前需要对浏览器特性进行检查,如下所示:

<script>
if ('loading' in HTMLImageElement . prototype){
//浏览器支持loading="lazy"的延迟加载方式
} else {
//获取其他JavaScript库来实现延迟加载
}
</script>

​ 当判断浏览器支持通过属性loading=”lazy”来进行延迟加载时,我们就在JavaScript处理程序中,将真实图像资源的URL赋值在其src属性上。而对于不支持该属性配置的延迟加载方式,就需要默认将真实图像资源的URL挂在data-src 属性上,仅当延迟加载的滚动事件触发时,才将data-src属性上的值换到src属性上。

​ 这也正是我们在传统方式中实现的加载策略,其原因是如果浏览器不支持标签的loading属性,便会立刻发起对src 属性上URL资源的网络请求。当然我们也可以使用CSS类名的方式触发对资源的加载。

< img data-src= “photo.jpg” loading=”lazy” class=”lazyload” alt=”photo” />不过对于这种方式,笔者建议等到loading属性在浏览器的稳定版本中被引入后,再在项目的生产环境中使用。

参考书籍:前端性能优化

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信