杰里米·瓦格纳
杰里米· 瓦格纳
Jeremy是Web Fundamentals的贡献者

网站的典型有效载荷中的图像 和视频部分 可能很重要。遗憾的是,项目利益相关者可能不愿意从现有应用程序中删除任何媒体资源。这种僵局令人沮丧,特别是当所有相关方都希望改善网站性能时,却无法就如何实现这一目标达成一致。幸运的是,延迟加载是一种降低初始页面有效负载 加载时间的解决方案,但不会吝啬内容。

什么是延迟加载?

延迟加载是在页面加载时延迟加载非关键资源的技术。相反,这些非关键资源在需要时加载。在涉及图像的地方,“非关键”通常与“屏幕外”同义。如果您使用过Lighthouse并检查了一些改进的机会,您可能会以Offscreen Images审核的形式看到这个领域的一些指导 :

灯塔中的屏幕外图像审核的屏幕截图。图1 。Lighthouse的性能审核之一是识别屏幕图像,这些图像是延迟加载的候选者。

你可能已经看到了延迟加载的动作,它是这样的:

  • 您到达页面,并在阅读内容时开始滚动。
  • 在某些时候,您将占位符图像滚动到视口中。
  • 占位符图像突然被最终图像替换。

图像延迟加载的一个示例可以在流行的发布平台 Medium上找到,它在页面加载时加载轻量级占位符图像,并在它们滚动到视口中时用延迟加载的图像替换它们。

浏览网站的屏幕截图,演示了延迟加载的动态。 模糊的占位符位于左侧,加载的资源位于右侧。图2 。图像延迟加载的一个例子。占位符图像在页面加载时加载(左),当滚动到视口中时,最终图像在需要时加载。

如果您不熟悉延迟加载,您可能想知道该技术有多么有用,以及它的好处是什么。请仔细阅读,找出答案!

为什么延迟加载图片或视频而不是加载它们?

因为你可能正在加载用户可能永远看不到的东西。由于以下几个原因,这是有问题的:

  • 它浪费了数据。在未计量的连接上,这不是可能发生的最糟糕的事情(尽管您可能正在使用这个宝贵的带宽来下载用户确实会看到的其他资源)。然而,在有限的数据计划中,加载用户从未见过的东西实际上可能是浪费他们的钱。
  • 它浪费了处理时间,电池和其他系统资源。下载媒体资源后,浏览器必须对其进行解码并在视口中呈现其内容。

当我们延迟加载图像和视频时,我们会减少初始页面加载时间,初始页面权重和系统资源使用,所有这些都会对性能产生积极影响。在本指南中,我们将介绍一些技术并为延迟加载图像和视频提供指导,以及一些常用库的简短列表。

懒惰加载图片

图像延迟加载机制在理论上很简单,但细节实际上有点挑剔。此外,还有一些不同的用例可以从延迟加载中受益。让我们首先从HTML中加载延迟加载内联图像开始。

内嵌图像

最常见的延迟加载候选是<img>元素中使用的图像。当我们延迟加载<img>元素时,我们使用JavaScript来检查它们是否在视口中。如果是,则他们src(有时srcset)属性将填充所需图像内容的URL。

使用交叉观察者

如果您之前编写过延迟加载代码,则可能已使用scrollor 等事件处理程序完成了任务resize。虽然这种方法在浏览器中最兼容,但现代浏览器提供了一种更高效,更有效的方法来通过交集观察器API来检查元素可见性。

注意: 并非所有浏览器都支持交叉观察者。如果跨浏览器的兼容性至关重要,请务必阅读下一节,其中介绍了如何使用性能较低(但更兼容!)的滚动和调整大小事件处理程序来延迟加载图像。

交叉点观察器比依赖各种事件处理程序的代码更容易使用和读取,因为开发人员只需要注册观察者来观察元素,而不是编写繁琐的元素可见性检测代码。剩下要为开发人员做的就是决定元素可见时要做什么。让我们假设这个基本的标记模式为我们的延迟加载<img> 元素:

 
 

<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!">

我们应该关注这个标记的三个相关部分:

  1. class属性,我们将在JavaScript中选择该元素。
  2. src属性,引用页面首次加载时将显示的占位符图像。
  3. data-srcdata-srcset属性,这是包含一旦元素中视,我们将加载图像的URL占位符属性。

现在让我们看看我们如何使用JavaScript中的交集观察器来使用此标记模式延迟加载图像:

 
 

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中查询<img>具有类的所有 元素lazy。如果交集观察者可用,我们创建一个新的观察者,当img.lazy元素进入视口时运行回调。查看此CodePen示例以查看此代码的实际运行情况。

注意: 此代码使用名为的交集观察方法isIntersecting,该方法 在Edge 15的交集观察器实现中不可用。因此,上面的延迟加载代码(和其他类似的代码片段)将失败。有关更完整的特征检测条件的指导,请参阅此GitHub问题。

然而,交叉观察者的缺点是虽然它在浏览器中有很好的支持,但它并不普遍。您需要填充 不支持它的浏览器,或者如上面的代码所示,检测它是否可用并随后回退到更旧,更兼容的方法。

使用事件处理程序(最兼容的方式)

虽然您应该使用交叉观察器进行延迟加载,但您的应用程序要求可能是浏览器兼容性至关重要的。您可以使用 polyfill交集观察器支持(这将是最简单的),但您也可以使用,以及可能与 事件处理程序一起 回退到代码 scroll, 以确定元素是否在视口中。resizeorientationchangegetBoundingClientRect

假设之前的标记模式相同,以下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);
});

此代码getBoundingClientRectscroll事件处理程序中使用,以检查img.lazy视口中是否有任何元素。甲setTimeout呼叫用来延迟处理,并且一个active变量包含其用于节流函数调用的处理的状态。由于图像是延迟加载的,因此它们将从元素数组中删除。当elements数组到达a length0,将删除滚动事件处理程序代码。请参阅此CodePen示例中的此代码。

虽然此代码几乎适用于任何浏览器,但它存在潜在的性能问题,即重复setTimeout调用可能会浪费,即使其中的代码受到限制也是如此。在此示例中,无论视口中是否有图像,都会在文档滚动或窗口大小调整上每200毫秒执行一次检查。另外,跟踪延迟加载多少元素和解除滚动事件处理程序绑定的繁琐工作留给了开发人员。

简单地说:尽可能使用交集观察器,如果最广泛的兼容性是关键应用程序要求,则回退到事件处理程序。

CSS中的图像

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

这种推测行为可用于通过使用JavaScript确定元素何时在视口内,并随后将类应用于应用样式调用背景图像的元素来推迟CSS中图像的加载。这导致在需要时而不是在初始加载时下载图像。例如,让我们采用一个包含大型英雄背景图像的元素:

 
 

<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调用的英雄背景图像。但是,在这个延迟加载示例中,我们可以通过 我们将在视口中添加到元素时添加的类来隔离div.lazy-background元素的background-image属性visible

 
 

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

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

从这里开始,我们将使用JavaScript来检查元素是否在视口中(带有交集观察者!),并在此时将该visible类添加到 div.lazy-background元素中,从而加载图像:

 
 

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);
    });
  }
});

如前所述,您需要确保为交叉观察者提供后备或填充,因为并非所有浏览器当前都支持它。查看此CodePen演示以查看此代码的实际运行情况。

懒加载视频

与图像元素一样,我们也可以延迟加载视频。当我们在正常情况下加载视频时,我们使用该<video>元素进行加载(尽管使用的替代方法 <img>已经出现了有限的实现)。我们如何延迟加载<video>取决于用例。让我们讨论一些场景,每个场景都需要不同的解决方案。

对于不自动播放的视频

对于用户启动播放的视频(即 自动播放的视频),可能需要 在元素上指定preload 属性<video>

 
 

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

在这里,我们使用preload值为的属性none来阻止浏览器预加载任何视频数据。为了占用空间,我们使用该poster 属性为<video>元素赋予占位符。原因是加载视频的默认行为因浏览器而异:

  • 在Chrome中,preload曾经是默认设置auto,但从Chrome 64开始,它现在默认为metadata。即使这样,在桌面版Chrome上,也可以使用Content-Range标题预加载部分视频。Firefox,Edge和Internet Explorer 11的行为类似。
  • 与桌面版Chrome一样,11.0桌面版Safari会预先加载一系列视频。在版本11.2(当前Safari的技术预览版)中,仅预加载视频元数据。在iOS上的Safari中,视频从不预先加载。
  • 当数据保护模式被激活,preload默认为none

因为关于浏览器的默认行为preload并非一成不变,所以明确可能是最好的选择。在用户启动播放的情况下,使用preload="none"是推迟在所有平台上加载视频的最简单方法。该preload属性不是推迟加载视频内容的唯一方法。使用视频预加载进行快速回放可以为您提供有关使用JavaScript进行视频回放的一些想法和见解。

不幸的是,当我们想要使用视频代替动画GIF时,它并不是很有用,我们将在下面介绍它们。

对于视频充当动画GIF替代品

虽然动画GIF广泛使用,但它们在很多方面都低于视频等效,特别是在输出文件大小方面。动画GIF可以扩展到几兆字节的数据范围。具有相似视觉质量的视频往往要小得多。

使用<video>元素作为动画GIF的替代并不像<img>元素那样简单。动画GIF中固有的是这三种行为:

  1. 它们在加载时自动播放。
  2. 它们不断循环(尽管并非总是如此)。
  3. 他们没有音轨。

使用<video>元素实现这一点看起来像这样:

 
 

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

autoplaymutedloop属性是不言自明的。 playsinline在iOS中进行自动播放是必要的。现在,我们有一个可维护的视频-GIF替代品,可跨平台运行。但是怎么去懒加载呢?Chrome会为您延迟加载视频,但您不能指望所有浏览器都提供此优化行为。根据您的受众和应用程序要求,您可能需要自己动手。首先,相应地修改<video>标记:

 
 

<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 属性,该属性允许您指定占位符以占用<video>元素的空间,直到视频延迟加载。与之前的<img>延迟加载示例一样,我们将视频URL存储data-src在每个<source> 元素的属性中。从那里,我们将使用一些类似于早期交叉观察者的图像延迟加载示例:

 
 

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);
    });
  }
});

当我们延迟加载<video>元素时,我们需要遍历所有子 <source>元素并将其data-src属性翻转为src属性。一旦我们完成了这个,我们需要通过调用元素的load方法来触发视频的加载,之后媒体将根据autoplay属性自动开始播放。

使用这种方法,我们有一个模拟动画GIF行为的视频解决方案,但不会像动画GIF那样产生相同的密集数据使用,我们会延迟加载该内容。

延迟加载库

如果你不太关心延迟加载是如何工作的,只是想选择一个库并去(并且没有羞耻!),有很多选项可供选择。许多库使用类似于本指南中演示的标记模式。以下是一些您可能会觉得有用的延迟加载库:

  • lazysizes是一个功能齐全的延迟加载库,延迟加载图像和iframe。它使用的模式与此处显示的代码示例非常相似,它自动绑定到元素上的 lazyload<img>,并要求您分别指定图像URL data-src和/或data-srcset属性,其内容分别交换到src和/或srcset属性中。它使用交叉点观察器(你可以填充),并且可以使用许多插件进行扩展,以执行延迟加载视频等操作。
  • lozad.js是一个超级轻量级​​选项,仅使用交集观察器。因此,它具有高性能,但在您可以在旧浏览器上使用之前需要进行填充。
  • blazy是另一个这样的选择,它将自己称为轻量级懒惰加载器(重量为1.4 KB)。与lazysizes一样,它不需要任何第三方实用程序来加载,并适用于IE7 +。不幸的是,它不使用交叉观察者。
  • yall.js是我编写的一个库,它使用IntersectionObserver并回退到事件处理程序。它与IE11和主流浏览器兼容。
  • 如果您正在寻找特定于React的延迟加载库,您可以考虑使用 react-lazyload。虽然它不使用交集观察器,但它确实为习惯于使用React开发应用程序的人提供了一种熟悉的延迟加载图像的方法。

这些延迟加载库中的每一个都有详细记录,为您的各种延迟加载工作提供了大量标记模式。如果你不是一个修修补补的人,那就去图书馆吧。这将花费最少的努力。

什么可能出错

虽然延迟加载图像和视频具有积极和可衡量的性能优势,但这不是一项轻松的任务。如果你弄错了,可能会产生意想不到的后果。因此,牢记以下问题非常重要:

记住这一点

使用JavaScript延迟加载页面上的每个媒体资源可能很诱人,但您需要抵制这种诱惑。在折叠上方休息的任何东西都不应该是懒惰的。这些资源应被视为关键资产,因此应正常加载。

以通常的方式加载关键媒体资源而不是延迟加载的主要参数是,延迟加载会延迟这些资源的加载,直到DOM在脚本完成加载并开始执行后处于交互状态之后。对于低于图像的图像,这很好,但使用标准<img>元素加载关键资源的速度会更快。

当然,如果在如此多的不同大小的屏幕上观看网站,那么折叠所在的地方就不那么清楚了。可能在于在笔记本电脑倍以上是什么样的下面它在移动设备上。在任何情况下都没有最佳解决方案的防弹建议。您需要对页面的关键资产进行清点,并以典型方式加载这些图像。

此外,您可能不希望对折叠线这么严格,因为触发延迟加载的阈值。为了您的目的,可能更理想的是在折叠下方一定距离处建立缓冲区,以便在用户将它们滚动到视口之前开始加载图像。例如,交集观察器API允许您rootMargin在创建新IntersectionObserver实例时在选项对象中指定属性。这有效地为元素提供了一个缓冲区,它在元素位于视口之前触发延迟加载行为:

 
 

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检查以包含缓冲区,您将在不支持交集观察器的浏览器中获得相同的效果。

布局转移和占位符

如果不使用占位符,延迟加载介质可能会导致布局移位。这些变化可能会使用户迷失方向并触发消耗系统资源并导致jank的昂贵的DOM布局操作。至少,请考虑使用占用与目标图像相同尺寸的纯色占位符,或者在加载之前提示媒体项内容的LQIP或 SQIP等技术 。

对于<img>标记,src最初应指向占位符,直到使用最终图像URL更新该属性。使用元素中的poster属性 <video>指向占位符图像。此外,在和标签上使用widthheight属性。这可确保从占位符到最终图像的转换不会在媒体加载时更改元素的渲染大小。<img><video>

图像解码延迟

在JavaScript中加载大图像并将其放入DOM可能会占用主线程,从而导致用户界面在解码过程中短时间内无响应。 在将图像decode插入DOM之前使用该 方法异步解码图像可以减少这种抖动,但要注意:它还无法在任何地方使用,并且它增加了延迟加载逻辑的复杂性。如果你想使用它,你需要检查它。下面显示了如何使用Image.decode()后备:

 
 

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

if ("decode" in newImage) {
  // Fancy decoding logic
  newImage.decode().then(function() {
    imageContainer.appendChild(newImage);
  });
} else {
  // Regular image load
  imageContainer.appendChild(newImage);
}

查看此CodePen链接,查看与此示例类似的代码。如果你的大多数图像都相当小,这对你来说可能没什么用,但是在延迟加载大图像并将它们插入DOM时,它肯定可以帮助减少jank。

什么东西不加载

有时媒体资源因某种原因无法加载,并且会发生错误。什么时候会发生?这取决于,但这是一个假设的场景:你有一个短暂的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始终可用。如果您要延迟加载图像,请考虑提供<noscript>标记,以便在JavaScript不可用时显示图像。最简单的回退示例涉及<noscript>在JavaScript关闭时使用元素来提供图像:

 
 

<!-- An image that eventually gets lazy loaded by JavaScript -->
<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load.jpg" alt="I'm an image!">
<!-- An image that is shown if JavaScript is turned off -->
<noscript>
  <img src="image-to-lazy-load.jpg" alt="I'm an image!">
</noscript>

如果JavaScript是关闭状态,用户将看到两个占位符图像,并包含与图像<noscript>元素。为了解决这个问题,我们可以像这样no-js<html>标签上放置一类:

 
 

<html class="no-js">

然后我们在<head>通过<link>标签请求任何样式表之前放置一行内联脚本, 如果JavaScript打开no-js则从<html>元素中删除该类:

 
 

<script>document.documentElement.classList.remove("no-js");</script>

最后,当JavaScript不可用时,我们可以使用一些CSS来简单地隐藏具有延迟类的元素,如下所示:

 
 

.no-js .lazy {
  display: none;
}

这不会阻止占位符图像加载,但结果更令人满意。关闭JavaScript的人获得的东西不仅仅是占位符图像,这比占位符更好,根本没有有意义的图像内容。

结论

使用时,延迟加载图像和视频可以严重降低您网站上的初始加载时间和页面有效负载。用户不会招致他们可能永远看不到的不必要的网络活动和媒体资源的处理成本,但他们仍然可以根据需要查看这些资源。

就性能改进技术而言,延迟加载是合理的无可争议的。如果您的网站中有大量内嵌图像,那么这是减少不必要下载的绝佳方式。您的网站的用户和项目利益相关者将非常感谢!

特别感谢FrançoisBeaufort,Dean Hume,Ilya Grigork,Paul Irish,Addy Osmani,Jeff Posnick和Martin Schierle的宝贵反馈,这些反馈显着提高了本文的质量。