防止延迟加载图像导致内容重排

Avatar of James Steinbach
James Steinbach

DigitalOcean 提供适合您旅程各个阶段的云产品。立即开始使用 $200 免费信用额度!

您知道 延迟加载图像 的概念。它可以防止浏览器加载图像,直到这些图像进入(或即将进入)浏览器的视窗。

有许多基于 JavaScript 的延迟加载解决方案。GitHub 上有超过 3,400 个不同的延迟加载存储库,而这些只是在可搜索字符串中包含“延迟加载”的存储库!它们大多数依赖于相同的技巧:不是将图像的 URL 放入 src 属性中,而是将其放入 data-src 中——这与响应式图像的模式相同。

  • JavaScript 监控用户向下滚动页面
  • 当用户遇到图像时,JavaScript 将 data-src 的值移到 src 中,使其归位。
  • 浏览器请求图像,并将其加载到视图中。

结果是浏览器在前端加载的图像更少,从而使页面加载更快。此外,如果用户从未滚动到足够的位置以查看图像,则该图像永远不会被加载。这等于更快的页面加载速度以及用户需要花费更少的数据。

您可能会想:“这太棒了!”。您说得对,这确实很神奇!

也就是说,它确实引入了一个明显的问题:不包含 src 属性的图像(包括其为空或无效时)没有高度。这意味着它们在页面布局中不会显示正确的尺寸,直到它们被延迟加载。

更新!时代变化很快,为了避免延迟加载的抖动,您只需在图像上添加正确的自然宽度和高度属性,它们就会很好地加载,即使 CSS 使图像具有流畅性。因此请像这样操作:<img src="image.jpg" width="800" height="600">

当用户滚动并延迟加载图像时,这些 img 元素的高度会从 0 像素变为它们所需的任何高度。这会导致重排,即图像下方或周围的内容会被推开,为新加载的图像腾出空间。重排是一个问题,因为它是一个阻塞用户的操作。它通过强制浏览器重新计算受该图像形状影响的任何元素的布局来减慢浏览器速度。CSS scroll-behavior 属性 可能会在某个时候有所帮助,但它的支持需要改进才能成为可行的选择。

延迟加载不能保证图像在进入视窗之前完全加载。结果是感觉体验卡顿,即使它是一个巨大的性能提升。

延迟加载图像还存在其他值得一提的问题,但不在本文的讨论范围之内。例如,如果 JavaScript 根本无法运行,则页面上将不会加载任何图像。这是任何基于 JavaScript 的解决方案的常见问题,但这篇文章只关注解决重排引入的问题。

如果我们能强制预加载的图像保持其正常的宽度和高度(即它们的纵横比),我们就可以防止重排问题,同时仍然延迟加载它们。这是我最近在 DockYard(我工作的地方)构建渐进式 Web 应用时必须解决的问题。

为了便于将来参考,有一个 名为 intrinsicsize 的 HTML 属性 旨在保留纵横比,但目前它只是 Chrome 中的实验性功能。

以下是我们的操作方法。

保持纵横比

保持纵横比的方法有很多。Chris 曾经 总结 过一个详尽的选项列表,但这里我们只关注特定于图像的选项。

图像本身

图像 src 提供了自然的纵横比。即使图像被响应式调整大小,它的自然尺寸仍然适用。以下是一段相当常见的响应式图像 CSS 代码

img {
  max-width: 100%;
  height: auto;
}

这段 CSS 代码告诉图像不要超过包含它们的元素的宽度,而是正确地缩放高度,以便在图像调整大小后不会出现“拉伸”或“挤压”。即使图像具有内联的 heightwidth 属性,这段 CSS 也会使它们在较小的视窗上保持良好的行为。

但是,如果还没有 src,则这种“自然纵横比”的行为就会失效。浏览器不关心 data-src,也不对其进行任何处理,因此它对于延迟加载重排来说不是一个可行的解决方案;但它对于理解图像加载后正常布局方式很重要。

伪元素

许多开发者(包括我自己)在尝试使用伪元素(例如 ::before::after)向 img 元素添加装饰时感到沮丧。浏览器不会渲染图像的伪元素,因为 img 是一个 替换元素,这意味着它的布局由外部资源控制。

但是,这个规则有一个例外:如果图像的 src 属性无效,浏览器将渲染它的伪元素。因此,如果我们将图像的 src 存储在 data-src 中,并且 src 为空,那么我们可以使用伪元素来设置纵横比

[data-src]::before {
  content: '';
  display: block;
  padding-top: 56.25%;
}

这将在具有 data-src 属性的任何元素的 ::before 上设置一个 16:9 的纵横比。一旦 data-src 变成 src,浏览器就会停止渲染 ::before,并且图像的自然纵横比将接管。

以下是一个演示

查看 CodePen 上 James Steinbach (@jdsteinbach) 创建的 图像纵横比:::before 填充

但是,这种解决方案有一些缺点。首先,它依赖于 CSS 和 HTML 协同工作。您的样式表需要为每个需要支持的图像纵横比都有一个声明。如果模板可以在没有 CSS 编辑的情况下插入图像,那就好多了。

其次,在撰写本文时,它在 Safari 12 及更低版本或 Edge 中无法正常工作。这是一个相当大的流量部分,会发送错误的布局。公平地说,保持纵横比是一种渐进式增强——最终渲染的页面并没有什么“错误”。但是,更理想的是解决重排问题,并使图像按预期渲染。

数据 URI(Base64)PNG

我们尝试保留纵横比的另一种方法是在 src 中内联 数据 URI,作为 PNG。使用 png-pixel.com 将有助于使用任何尺寸和颜色进行所有这些 base64 编码的操作。这可以直接进入 HTML 中图像的 src 属性

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAQAAAA3fa6RAAAADklEQVR42mNkAANGCAUAACMAA2w/AMgAAAAASUVORK5CYII=" data-src="//picsum.photos/900/600" alt="Lazy loading test image" />

那里内联的 PNG 有一个 3:2 的纵横比(与最终图像相同的纵横比)。当 srcdata-src 的值替换时,图像将像我们想要的那样保持其纵横比!

以下是一个演示

查看 CodePen 上 James Steinbach (@jdsteinbach) 创建的 图像纵横比:内联 base64 PNG

是的,这种方法也有一些缺点。尽管浏览器支持更好,但它很难维护。我们需要为每个新的图像尺寸生成一个 base64 字符串,然后使这个字符串对象可用于正在使用的任何模板工具。它也不是表示此数据的最高效方法。

我继续探索,发现了一个更小的方法。

将 SVG 与 base64 结合使用

在探索了内联 PNG 选项之后,我想知道 SVG 是否可能是内联图像的更小格式,以下是我发现的:具有 viewBox 声明的 SVG 是一个占位符图像,具有易于编辑的原生纵横比。

首先,我尝试对 SVG 进行 base64 编码。以下是我在 HTML 中使用的示例

<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzIDInPjwvc3ZnPg==" data-src="//picsum.photos/900/600" alt="Lazy loading test image">

在较小的简单纵横比上,这与 base64 PNG 的大小大致相同。1:1 的比例将是使用 base64 PNG 的 114 字节,使用 base64 SVG 的 106 字节。2:3 的比例将是使用 base64 PNG 的 118 字节,使用 base64 SVG 的 106 字节。

但是,使用 base64 SVG 对于较大、更复杂的比例仍然很小,这在文件大小方面是一个真正的赢家。16:9 的比例使用 base64 PNG 为 122 字节,使用 base64 SVG 为 110 字节。923:742 的比例使用 base64 PNG 为 3,100 字节,而使用 base64 SVG 仅为 114 字节!(这不是一个常见的纵横比,但我需要使用客户用例中的自定义尺寸进行测试。)

以下是一个表格,更清晰地显示这些比较

纵横比 base64 PNG base64 SVG
1:1 114 字节 106 字节
2:3 118 字节 106 字节
16:9 122 字节 110 字节
923:742 3,100 字节 114 字节

对于简单的比例,差异微不足道,但您可以看到随着比例变得复杂,SVG 的缩放能力非常出色。

我们现在有了更好的浏览器支持。这项技术得到了所有主要参与者的支持,包括 Chrome、Firefox、Safari、Opera、IE11 和 Edge,而且在移动浏览器中也有很好的支持,包括 Safari iOS、Chrome for Android 和 Samsung for Android(从 4.4 开始)。

以下是一个演示

查看 CodePen 上 James Steinbach (@jdsteinbach) 的笔 图像纵横比:内联 base64 SVG

🏆 我们有赢家了!

是的,我们有,但请继续关注,因为我们将进一步改进这种方法!我记得 Chris 建议我们不要在 CSS 背景图像中使用 base64 编码的 SVG,并认为该建议也可能适用于这里。

在这种情况下,我没有对 SVG 进行 base64 编码,而是使用了该文章中的“优化的 URL 编码”技术。以下是标记:

<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3C/svg%3E" data-src="//picsum.photos/900/600" alt="Lazy loading test image" />

这比 base64 SVG 稍微小一点。1:1 在 base64 中是 106 字节,在 URL 编码中是 92 字节。16:9 在 base64 中输出 110 字节,在 URL 编码中输出 97 字节。

如果您对不同文件和编码格式的更多数据大小感兴趣,此演示 比较了所有这些技术之间的不同字节大小。

但是,使 URL 编码的 SVG 成为明显赢家的真正优势在于它的格式是人类可读的、易于模板化的,并且可以无限定制!

您不需要创建一个 CSS 块 或者生成一个 base64 字符串来获取一个完美的图像占位符,其中尺寸是未知的!例如,以下是一个使用此技术的 React 组件:

const placeholderSrc = (width, height) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`

const lazyImage = ({url, width, height, alt}) => {
  return (
    <img
      src={placeholderSrc(width, height)}
      data-src={url}
      alt={alt} />
  )
}

查看 CodePen 上 James Steinbach (@jdsteinbach) 的笔 React 延迟加载图像,具有稳定的纵横比

或者,如果您更喜欢 Vue:

查看 CodePen 上 James Steinbach (@jdsteinbach) 的笔 Vue 延迟加载图像,具有稳定的纵横比

我很高兴地报告,浏览器支持并没有随着这种改进而改变 - 我们仍然拥有与 base64 SVG 一样的完整支持!

结论

我们探索了几种技术来通过在交换发生之前保留延迟加载图像的纵横比来防止内容重排。我能够找到的最佳技术是内联和优化的 URL 编码 SVG,其图像尺寸在 viewBox 属性中定义。这可以用这样的函数进行脚本编写:

const placeholderSrc = (width, height) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`

此技术有几个优势:

  • 在台式机和移动设备上具有可靠的浏览器支持
  • 最小的字节大小
  • 人类可读的格式
  • 无需运行时编码调用即可轻松模板化
  • 无限可扩展性

您如何看待这种方法?您是否使用过类似的方法,或者有完全不同的处理重排的方法?请告诉我!