性能优化(5)-如何推迟、延迟加载和与交叉观察者一起行动

什么是延迟加载(Lazy-Load)?

在网站典型负载中,图像和视频是非常重要的一部分内容。 不过遗憾的是,项目干系人并不愿意从其现有应用中删除任何媒体资源。 这种僵局不免令人沮丧,尤其是当所有相关方都希望改善网站性能,但却无法就具体方法达成一致时。 幸运的是,延迟加载解决方案可以减少初始页面负载和加载时间,但不会删减任何内容。

延迟加载是一种在加载页面时,延迟加载非关键资源的方法, 而这些非关键资源则在需要时才进行加载。 就图像而言,“非关键”通常是指“屏幕外”。

您可能已经见过延迟加载的实际应用,其过程大致如下:

  • 您访问一个页面,并开始滚动阅读内容。
  • 在某个时刻,您将占位符图像滚动到视口中。
  • 该占位符图像瞬间替换为最终图像。

您可以在热门发布平台 Medium 上找到图像延迟加载的示例。该平台在加载页面时会先加载轻量级的占位符图像,并在其滚动到视口时,将之替换为延迟加载的图像。

在这里插入图片描述
图 2. 图像延迟加载实际应用示例。 占位符图像在页面加载时加载(左侧),当滚动到视口时,最终图像随即加载(即在需要时加载)。

为何要延迟加载图像或视频,而不是直接加载?

因为直接加载可能会加载用户永远不会查看的内容, 进而导致一些问题:

  • 浪费数据流量。 如果使用无限流量网络,这可能还不是最坏的情况(不过,这些宝贵的带宽原本可以用来下载用户确实会查看的其他资源)。 但如果流量有限,加载用户永远不会查看的内容实际上是在浪费用户的金钱。
  • 浪费处理时间、电池电量和其他系统资源。 下载媒体资源后,浏览器必须将其解码,并在视口中渲染其内容。

延迟加载图像和视频时,可以减少初始页面加载时间初始页面负载以及系统资源使用量,所有这一切都会对性能产生积极影响。

IntersectionObserver API的引入

女士们先生们,让我们来谈谈交叉观察者 API。 但是在我们开始之前,让我们先来看看现代工具的前景,正是这些工具将我们引向了 IntersectionObserver。

2017年对于内置在浏览器中的工具来说是非常好的一年,它帮助我们不费吹灰之力地提高了代码库的质量和风格。 如今,网络似乎正在摆脱基于完全不同于解决非常典型的观察者界面(或者仅仅是“观察者”)方法的零星解决方案: 支持良好的MutationObserver获得了新的家庭成员,这些家庭成员很快被现代浏览器所采用:

另一个潜在的家庭成员,FetchObserver,是一个正在进行的工作,并引导我们更多地进入网络代理的土地,但今天我想谈论更多的是交叉观察者 。
在这里插入图片描述
Performanceobserver 和 IntersectionObserver 旨在帮助前端开发人员提高项目在不同方面的性能。 前者为我们提供了真实用户监控工具,而后者是工具,为我们提供了切实的性能改进

为了特别了解 IntersectionObserver 的机制,我们应该看看一个普通的观察者在现代网络中是如何工作的。

Observer Vs. Event(观察者 Vs 事件)

顾名思义,“观察者”意在观察页面上下文中发生的事情。 观察者可以观察页面上发生的事情,比如 DOM 的变化。 他们还可以观察页面的生命周期事件。 观察者还可以运行一些回调函数。 现在,细心的读者可能会立刻发现这里的问题,并问道: “那么,重点是什么呢? 我们不是已经有这方面的事件了吗? 是什么让观察者与众不同? ” 说得好! 让我们仔细看看,把它分类出来。

常规事件和观察者之间的关键区别在于,默认情况下,前者对事件的每次发生都进行同步响应,影响主线程的响应,而后者应该异步响应,而不会对性能造成太大影响。 至少,这对于当前呈现的观察者来说是正确的: 所有的观察者都异步运行,我认为这在将来不会改变。

这就导致了在处理观察者回调时的主要差异,这可能会使初学者感到困惑: 观察者的异步特性可能会导致同时将多个观察值传递给一个回调函数。 因此,回调函数应该期望不是单个entry,而是一组entries(即使有时 Array 中只包含一个条目)。

此外,一些观察者(特别是我们今天讨论的那个)提供了非常方便的预计算属性,否则,我们在使用常规事件时使用昂贵的(从性能角度)方法和属性来计算自己。

但是,无论您喜欢哪种定义,观察者都不应该用来替代事件(至少现在还不是) ; 两者都有足够的用例,而且他们可以并肩生活。

在这里插入图片描述

通用观察者的结构
/**
* 通用观察者的结构
*/
let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) {
  // entries: 观察到的元素数组 
  entries.forEach(entry => {
      // 在这里,我们可以做一些事情,每个特定的entry 
  });
});

// 现在我们应该告诉我们的观察者去观察什么元素
observer.observe(WHAT-TO-OBSERVE);

再次注意,entries是一个值的数组,而不是一个单独的entry。

这就是泛型结构: 特定观察者的实现在传递给它的 observe ()的参数和传递给它的回调的参数上有所不同。 例如,MutationObserver 还应该获得一个配置对象,以便更多地了解 DOM 中要观察的更改。 Performanceobserver 不在 DOM 中观察节点,而是具有它可以观察到的一组专用entry类型。

在这里,让我们结束这个讨论的“一般”部分,并深入探讨今天文章的主题—— IntersectionObserver

IntersectionObserver API
什么是交叉观察者。

在这里插入图片描述
按照 MDN 的说法:
Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档视图交叉处变化的方法。

简单地说,IntersectionObserver 异步地观察一个元素与另一个元素的重叠。 让我们来谈谈这些元素在 IntersectionObserver 中的作用。

交叉观察者初始化

在前面的一段中,我们看到了一般观察者的结构。 Intersectionobserver 对这个结构进行了一些扩展。 首先,这种类型的观察者需要一个包含三个主要元素的配置:

  • root:这是用于观察的根元素。 它定义了可观测元素的基本“捕获框架”。 默认情况下,root 是浏览器的 viewport,但实际上可以是 DOM 中的任何元素(然后将 root 设置为 document.getElementById (‘ your-element’))。 但请记住,在这种情况下,您想要观察的元素必须“存在”于根的 DOM 树中。
    在这里插入图片描述

  • rootMargin:定义根元素周围的边距,当根元素的尺寸不能提供足够的灵活性时,该边距可以扩展或缩小“捕获框架”。 这个配置值的选项类似于 CSS 中的页边距,比如 rootMargin: ’50px 20px 10px 40px’(顶部,右测,下部,左侧)。 当然你也可以缩写(如 rootMargin: “50px”) ,并且可以用 px 或% 表示。 默认情况下,rootMargin: ‘0px’。
    在这里插入图片描述

  • threshold:当被观察的元素跨过“捕获框架”(定义为根和 rootMargin 的组合)的边界时,并不总是希望立即作出反应。 阈值定义了这样的交叉点的百分比,观察者应作出反应。 它可以定义为单个值或值数组。 为了更好地理解阈值的效果(我知道有时可能会令人困惑) ,这里有一些例子:

  • threshold: 0:当观察到的元素的与“捕获框架”的边界(上边界,下边界)之一相交时,(threshold默认值为0) IntersectionObserver 应该作出反应 请记住,IntersectionObserver 是方向不可知的,这意味着它将在两种情况下作出反应:
    a)当元素进入时;
    b)当元素离开“捕获框架”时

  • threshold: 0.5:当被观察元素的50% 与“捕获框架”相交时,观察者应被激发。

  • threshold: [0, 0.2, 0.5, 1]:当被观察元素与边界相交时、当有20%进入“捕获框架”
    时、当有50%进入“捕获框架”时、当100%进入“捕获框架”时观察者会被激发。
    在这里插入图片描述
    为了告诉 IntersectionObserver 我们想要的配置,我们只需将我们的 options 对象和回调函数一起传递到 Observer 的构造函数中,如下所示:

const options = {
  root: null, // 将其设置为默认值:viewport 
  rootMargin: '0px',
  threshold: 0.5
};
let observer = new IntersectionObserver(function(entries) {}, options);

现在,我们应该给予 IntersectionObserver 实际要观察的元素。 这只需要传递一个元素给 observe ()函数:

const img = document.getElementById('image-to-observe');
observer.observe(image);

关于这个观察到的元素,需要注意以下几点:

  • 前面已经提到过,但是值得再次提到: 如果你设置root为 DOM 中的一个元素,所观察到的元素应该位于root。
  • IntersectionObserver一次只能接受一个元素进行观测,不支持批量供应观测。 这意味着,如果你需要观察几个元素(比如说一个页面上有几张图片) ,你必须遍历所有这些元素,并分别观察每个元素。
const images = document.querySelectorAll('img');
images.forEach(image => {
    observer.observe(image);
});

交叉观察者回调
let observer = new IntersectionObserver(callback, options);

首先,IntersectionObserver 的回调函数接受两个参数,我们将从第二个参数开始以颠倒的顺序讨论这两个参数。 除了前面提到的条目 Array,与我们的“捕获框架”相交之外,回调函数还将 Observer 本身作为第二个参数。

在很多情况下,当你想要停止观察某个被 IntersectionObserver 首次检测到的元素时,获得对观察者本身的引用是非常有用的。 像图像的延迟加载、其他资产的延迟获取等等都属于这种情况。 当您想要停止观察一个元素时,IntersectionObserver 提供了一个 unobserve (element-to-stop-observing)方法,可以在对被观察的元素执行一些操作之后在回调函数中运行(比如实际延迟加载图像)。

我们在回调函数中获得的作为 Array 的entries属于特殊类型: IntersectionObserverEntry。 这个界面为我们提供了一个预先定义和预先计算的关于每个特定观测元素的属性集。 让我们来看看最有趣的一些。

首先,IntersectionObserverEntry 类型的 entries包含三个不同的矩形的信息ーー定义过程中涉及的元素的坐标和边界:

  • rootBounds:“捕获框架”(root + rootMargin)的矩形
  • boundingClientRect:观察元素本身的矩形
  • intersectionRect:被观察元素相交的“捕获框架”的一个区域

在这里插入图片描述
为我们异步计算这些矩形最酷的地方在于,它提供了与元素定位相关的重要信息,而不需要我们调用 getBoundingClientRect ()、 offsetTop、 offsetLeft 和其他昂贵的定位属性和方法来触发布局(回流)。

我们感兴趣的 IntersectionObserverEntry 接口的另一个属性是 intersecting。 这是一个便利属性,用于指示被观察的元素当前是否与“捕获框架”相交。 当然,我们可以通过查看相交矩形(如果这个矩形不是0x0,元素与“捕获框架”相交)来获得这些信息,但是为我们预先计算这些信息是相当方便的。

Isintersecting 可以用来确定被观察的元素是刚刚进入“捕获框架”还是已经离开了它。 要找出这一点,保存这个属性的值作为一个全局标志,当这个元素的entry到达你的回调函数时,比较它的新 isIntersecting 和那个全局标志:

Isintersecting 正是帮助我们解决前面讨论的问题的属性。

let isLeaving = false;
let observer = new IntersectionObserver(function(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 我们正在进入“捕获框架”。设置 flag
      isLeaving = true;
      // 做点什么
    } else if (isLeaving) {
      // 我们正在退出“捕获框架” 
      isLeaving = false;
      // 做点什么
    }
  });
}, options);

以上只针对单个观察。

Intersectionobserverentry 接口提供了另一个预先计算的便利属性: intersectionRatio。 此参数可用于与 isIntersecting 相同的目的,但由于它是浮点数而不是布尔值,因此提供了更细粒度的控制。 Intersectionratio 的值表示观察元素的区域与“捕获框架”(intersectionRect 区域与 boundingClientRect 区域的比值)的相交程度。 同样的,我们可以自己用这些矩形的信息来计算,但是有这样的信息来帮助我们是很好的。
在这里插入图片描述
Target 是 IntersectionObserverEntry 接口的另一个属性,您可能需要经常访问它。 但是这里绝对没有魔法——它只是传递给 Observer ()函数的原始元素。 就像 event.target 在处理事件时已经习惯的那样。
要获得 IntersectionObserverEntry 接口的完整属性列表,请查看规范

为什么交叉观察者对你有好处?
  • IntersectionObserver是一个异步非阻塞的 API
  • IntersectionObserver取代我们昂贵的scroll或resize事件
  • IntersectionObserver所有昂贵的计算你都不需要
一些常见的应用
延迟功能

首先,让我们回顾一个示例,它揭示了 IntersectionObserver 概念的基本原理。 假设您有一个元素,一旦它出现在屏幕上,就必须进行大量计算。 例如,您的广告应该注册一个视图,只有当它实际上已经显示给用户。 但是现在,让我们假设在页面的第一个屏幕下方有一个自动播放的 轮播图元素。

在这里插入图片描述
一般来说,运行轮播图是一项繁重的任务。 通常,它涉及到 JavaScript 计时器、自动滚动元素的计算等等。 所有这些任务都加载主线程,当它在自动播放模式下运行时,我们很难知道我们的主线程何时受到阻塞。 被阻塞的主线程成为我们性能的瓶颈。为了解决这个问题,我们可以推迟回放这样的轮播图,直到它进入浏览器的视口。 对于这种情况,我们将使用我们的知识和示例来处理 IntersectionObserverEntry 接口的 isIntersecting 参数。

const carousel = document.getElementById('carousel');
let isLeaving = false;
let observer = new IntersectionObserver(function(entries) {
  entries.forEach(entry => {
        if (entry.isIntersecting) {
          isLeaving = true;
          entry.target.startCarousel();
        } else if (isLeaving) {
          isLeaving = false;
          entry.target.stopCarousel();
        }
    });
}
observer.observe(carousel);

在这里,只有当旋转木马进入我们的 viewport 时,我们才播放它。 注意,在 IntersectionObserver 的初始化中没有配置对象: 这意味着我们依赖于默认的配置选项。 当 carousel 离开我们的 viewport 时,我们应该停止播放它,不要在不再重要的元素上花费资源。

延迟加载图像

从理论上来看,图像延迟加载机制十分简单,但实际上却有很多需要注意的细节。 此外,有多个不同的用例均受益于延迟加载。 首先,我们来了解一下在 HTML 中延迟加载内联图像。

内联图像

<img> 元素中使用的图像是最常见的延迟加载对象。 延迟加载 <img> 元素时,我们使用 JavaScript 来检查其是否在视口中。 如果元素在视口中,则其 src(有时是 srcset)属性中就会填充所需图像内容的网址。

如果您曾经编写过延迟加载代码,您可能是使用 scroll 或 resize 等事件处理程序来完成任务。 虽然这种方法在各浏览器之间的兼容性最好,但是现代浏览器支持通过 Intersection Observer API 来检查元素的可见性,这种方式的性能和效率更好。

注:并非所有浏览器都支持 Intersection Observer。 如果浏览器之间的兼容性至关重要,如何使用性能稍差(但兼容性更好!)的 scroll 和 resize 事件处理程序来延迟加载图像。

与依赖于各种事件处理程序的代码相比,Intersection Observer 更容易使用和阅读。这是因为开发者只需要注册一个 Observer 即可监视元素,而无需编写冗长的元素可见性检测代码。 然后,开发者只需要决定元素可见时需要做什么即可。

假设我们的延迟加载 元素采用以下基本标记模式:

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">

在此标记中,我们应关注三个相关部分:

  • class 属性,这是我们在 JavaScript 中选择元素时要使用的类选择器。
  • src 属性,引用页面最初加载时显示的占位符图像。
  • data-src 和 data-srcset 属性,属于占位符属性,其中包含元素进入视口后要加载的图像的网址。
    现在,我们来看看如何在 JavaScript 中使用 Intersection Observer,并通过以下标记模式延迟加载图像:
document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to a more compatible method here
  }
});

在文档的 DOMContentLoaded 事件中,此脚本会查询 DOM,以获取类属性为 lazy 的所有 元素。 如果 Intersection Observer 可用,我们会创建一个新的 Observer,以在 img.lazy 元素进入视口时运行回调。

不过,Intersection Observer 的缺点是虽然在浏览器之间获得良好的支持,但并非所有浏览器皆提供支持。 对于不支持 Intersection Observer 的浏览器,您可以使用 polyfill,或者如以上代码所述,检测 Intersection Observer 是否可用,并在其不可用时回退到兼容性更好的旧方法。

使用事件处理程序(兼容性最好的方法)虽然您应该使用 Intersection Observer 来执行延迟加载,但您的应用可能对浏览器的兼容性要求比较严格。 您可以使用 polyfil 为不支持 Intersection Observer 的浏览器提供支持(这种方法最简单),但也可以回退到使用 scroll 和 resize的代码,甚至回退到与 getBoundingClientRect 配合使用的 orientationchange 事件处理程序,以确定元素是否在视口中。

假定使用与上文相同的标记模式,以下 JavaScript 可提供延迟加载功能:

document.addEventListener("DOMContentLoaded", function() {
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  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") {
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.srcset = lazyImage.dataset.srcset;
            lazyImage.classList.remove("lazy");

            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 事件处理程序中使用 getBoundingClientRect 来检查是否有任何 img.lazy 元素处于视口中。 使用 setTimeout 调用来延迟处理,active 变量则包含处理状态,用于限制函数调用。 延迟加载图像时,这些元素随即从元素数组中移除。 当元素数组的 length 达到 0 时,滚动事件处理程序代码随即移除。

虽然此代码几乎可在任何浏览器中正常运行,但却存在潜在的性能问题,即重复的 setTimeout 调用可能纯属浪费,即使其中的代码受限制,它们仍会运行。 在此示例中,当文档滚动或窗口调整大小时,不管视口中是否有图像,每 200 毫秒都会运行一次检查。 此外,跟踪尚未延迟加载的元素数量,以及取消绑定滚动事件处理程序的繁琐工作将由开发者来完成。

简而言之:请尽可能使用 Intersection Observer,如果应用有严格的兼容性要求,则回退到事件处理程序。

CSS 中的图像

虽然 标记是在网页上使用图像的最常见方式,但也可以通过 CSS background-image 属性(以及其他属性)来调用图像。 与加载时不考虑可见性的 元素不同,CSS 中的图像加载行为是建立在更多的推测之上。 构建文档和 CSS 对象模型以及渲染 树后,浏览器会先检查 CSS 以何种方式应用于文档,再请求外部资源。 如果浏览器确定涉及某外部资源的 CSS 规则不适用于当前构建中的文档,则浏览器不会请求该资源。

这种推测性行为可用来延迟 CSS 中图像的加载,方法是使用 JavaScript 来确定元素在视口内,然后将一个类应用于该元素,以应用调用背景图像的样式。 如此即可在需要时而非初始加载时下载图像。 例如,假定一个元素中包含大型主角背景图片:

<div class="lazy-background">
  <h1>Here's a hero heading to get your attention!</h1>
  <p>Here's hero copy to convince you to buy a thing!</p>
  <a href="/buy-a-thing">Buy a thing!</a>
</div>

div.lazy-background 元素通常包含由某些 CSS 调用的大型主角背景图片。 但是,在此延迟加载示例中,我们可以通过 visible 类来隔离 div.lazy-background 元素的 background-image 属性,而且我们会在元素进入视口时对其添加这个类:

.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}

.lazy-background.visible {
  background-image: url("hero.jpg"); /* The final image */
}

我们将从这里使用 JavaScript 来检查该元素是否在视口内(通过 Intersection Observer 进行检查!),如果在视口内,则对 div.lazy-background 元素添加 visible 类以加载该图像:

document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));

  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });

    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});
延迟加载视频

与图像元素一样,视频也可以延迟加载。 在正常情况下加载视频时,我们使用的是 元素(尽管也可以改为使用 ,不过实现方式受限)。 但是,延迟加载 的方式取决于用例。 下文探讨的几种情况所需的解决方案均不相同。

视频不自动播放

对于需要由用户启动播放的视频(即不自动播放的视频),最好指定 元素的 preload 属性:

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

这里,我们使用值为 none 的 preload 属性来阻止浏览器预加载任何视频数据。 为占用空间,我们使用 poster 属性为 元素提供占位符。 这是因为默认的视频加载行为可能会因浏览器不同而有所不同。

由于浏览器在 preload 方面的默认行为并非一成不变,因此您最好明确指定该行为。 在由用户启动播放的情况下,使用 preload=“none” 是在所有平台上延迟加载视频的最简单方法。 但 preload 属性并非延迟加载视频内容的唯一方法。

用视频代替动画 GIF

虽然动画 GIF 应用广泛,但其在很多方面的表现均不如视频,尤其是在输出文件大小方面。 动画 GIF 的数据大小可达数兆字节, 而视觉效果相当的视频往往小得多。
使用 元素代替动画 GIF 并不像使用 元素那么简单。 动画 GIF 具有以下三种固有行为:

  • 加载时自动播放。
  • 连续循环播放
  • 没有音轨。

使用 元素进行替代类似于:

<video autoplay muted loop playsinline>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

autoplay、muted 和 loop 属性的含义不言而喻,而 playsinline 是在 iOS 中进行自动播放所必需。 现在,我们有了可以跨平台使用的“视频即 GIF”替代方式。 但是,如何进行延迟加载?Chrome 会自动延迟加载视频,但并不是所有浏览器都会提供这种优化行为。 根据您的受众和应用要求,您可能需要自己手动完成这项操作。 首先,请相应地修改 标记:

<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

您会发现添加了 poster 属性,您可以使用该属性指定占位符以占用 元素的空间,直到延迟加载视频为止。 与上文中的 延迟加载示例一样,我们将视频网址存放在每个 元素的 data-src 属性中。 然后,我们将使用与上文基于 Intersection Observer 的图像延迟加载示例类似的 JavaScript:

document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

延迟加载 元素时,我们需要对所有的 子元素进行迭代,并将其 data-src 属性更改为 src 属性。 完成该操作后,必须通过调用该元素的 load 方法触发视频加载,然后该媒体就会根据 autoplay 属性开始自动播放。

利用这种方法,我们即可提供模拟动画 GIF 行为的视频解决方案。这种方案的流量消耗量低于动画 GIF,而且能延迟加载内容。

可能出错的地方
注意首屏

使用 JavaScript 对页面上的所有媒体资源进行延迟加载很诱人,但您必须抵挡住这种诱惑。 首屏上的任何内容皆不可进行延迟加载, 而应将此类资源视为关键资产,进行正常加载。

以正常而非延迟加载方式加载关键媒体资源的主要理据是,延迟加载会将这些资源的加载延迟到 DOM 可交互之后,在脚本完成加载并开始执行时进行。 对于首屏线以下的图像,可以采用延迟加载,但对于首屏上的关键资源,使用标准的 元素来加载速度会快得多。

当然,如今用来查看网站的屏幕多种多样,且大小各有不同,因此首屏线的具体位置并不明确。 笔记本电脑上位于首屏的内容在移动设备上可能位于首屏线以下。 目前并没有完全可靠的建议,无法在每种情况下完美解决这个问题。 您需要清点页面的关键资产,并以典型方式加载这些图像。

此外,您可能也不想严格限定首屏线作为触发延迟加载的阈值。 对您来说,更理想的做法是在首屏线以下的某个位置建立缓冲区,以便在用户将图像滚动到视口之前,即开始加载图像。 例如,Intersection Observer API 允许您在创建新的 IntersectionObserver 实例时,在 options 对象中指定 rootMargin 属性。 如此即可为元素提供缓冲区,以便在元素进入视口之前触发延迟加载行为:

let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
  // Lazy loading image code goes here
}, {
  rootMargin:"0px 0px 256px 0px"
});

如果 rootMargin 的值与您为 CSS margin 属性指定的值相似,这是因为该值就是如此!在本例中,我们将观察元素(默认情况下为浏览器视口,但可以使用 root 属性更改为特定的元素)的下边距加宽 256 个像素。 这意味着,当图像元素距离视口不超过 256 个像素时,回调函数将会执行,即图像会在用户实际看到它之前开始加载。

要使用滚动事件处理代码实现这种效果,只需调整 getBoundingClientRect 检查以包括缓冲区,如此一来,您即可在不支持 Intersection Observer 的浏览器上获得相同效果。

布局移位与占位符

若不使用占位符,延迟加载媒体可能会导致布局移位。 这种变化不仅会让用户产生疑惑,还会触发成本高昂的 DOM 布局操作,进而耗用系统资源,造成卡顿。 您至少应考虑使用纯色占位符来占用尺寸与目标图像相同的空间,或者采用 LQIP 或 SQIP 等方法,在媒体项目加载前提供有关其内容的提示。

对于 标记,src 最初应指向一个占位符,直到该属性更新为最终图像的网址为止。 请使用 元素中的 poster 属性来指向占位符图像。 此外,请在 和 标记上使用 width 和 height 属性。 如此可以确保从占位符转换为最终图像时,不会在媒体加载期间改变该元素的渲染大小。

内容不加载

有时,媒体资源会因为某种原因而加载失败,进而导致发生错误。 何时会发生这种情况?何时发生视情况而定,以下是一种假设情况: 您有一个短时间(例如,5 分钟)的 HTML 缓存策略,而用户访问网站,或保持打开旧选项卡并长时间离开(例如,数个小时),然后返回继续阅读内容。 在此过程中的某个时刻,发生重新部署。 在此部署期间,图像资源的名称因为基于哈希的版本控制而更改,或者完全移除。 当用户延迟加载图像时,该资源已不可用,因此导致加载失败。

虽然出现这种情况的机会比较小,但您也有必要制定后备计划,以防延迟加载失败。 对于图像,可采取如下解决方案:

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

newImage.onerror = function(){
  // Decide what to do on error
};
newImage.onload = function(){
  // Load the image
};

发生错误时采取何种措施取决于应用。 例如,可以将图像占位符区域替换为按钮,以允许用户尝试重新加载该图像,或者直接在图像占位符区域显示错误消息。

此外,也可能会发生其他情况。 无论采取何种方法,在发生错误时通知用户,并提供可能的解决方案总不是坏事。

JavaScript 可用性

不应假定 JavaScript 始终可用。 如果要延迟加载图像,请考虑提供 标记,以便在 JavaScript 不可用时显示图像。

结论

该方法可以显著减少网站上的初始加载时间和页面负载。 用户不查看的媒体资源不会为其带来不必要的网络活动和处理成本,但用户可以根据需要查看这些资源。

就性能改进方法而言,延迟加载无可争议。 如果您的网站上存在大量内联图像,这是减少非必要下载的好方法。 您的网站用户和项目干系人都会因该方法而受益匪浅!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值