您知道 延迟加载图像 的概念。它可以防止浏览器加载图像,直到这些图像进入(或即将进入)浏览器的视窗。
有许多基于 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 代码告诉图像不要超过包含它们的元素的宽度,而是正确地缩放高度,以便在图像调整大小后不会出现“拉伸”或“挤压”。即使图像具有内联的 height
和 width
属性,这段 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 的纵横比(与最终图像相同的纵横比)。当 src
被 data-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`
此技术有几个优势:
- 在台式机和移动设备上具有可靠的浏览器支持
- 最小的字节大小
- 人类可读的格式
- 无需运行时编码调用即可轻松模板化
- 无限可扩展性
您如何看待这种方法?您是否使用过类似的方法,或者有完全不同的处理重排的方法?请告诉我!
就个人而言,我使用一个围绕我的图像的包装器,并使用我编写的“实用”类(功能性 CSS)。
类似这样
以及我定义的实用类
“fluid-holder” 是一个 Sass 函数,它根据我传递给参数的比率设置正确的填充。
我已经在我的 WordPress 博客上为 GIF 使用了这种 SVG 技术一段时间了:我使用生成的 JPEG 缩略图作为
<img>
的背景图像,并将 GIF 的 URL 放置在data-gif-url
属性中。当我在博客文章中插入它时,src
属性与 SVG 的数据 URI 一起动态生成(使用一个小的自定义 Gutenberg 块来读取文件的属性)。然后,我只需叠加一个按钮,该按钮切换src
和data-gif-url
属性,就这样!这比不得不将每个 GIF 调整为特定的纵横比要容易得多。(是的,我知道用 GIF 代替视频文件不好......我以后会找到解决方案)
您可以通过自闭合根
<svg>
元素使最终代码片段更小。......但是为什么不直接使用内联高度和宽度属性呢?这是最语义化的解决方案,并且保证有效。更好的是,它允许您在图像开始加载之前使用 1 像素透明 png 数据 URL 作为占位符。
这似乎没有按描述的那样工作:我在 CodePen 中尝试了,1 像素透明 png 数据 URL 覆盖了高度/宽度属性:https://codepen.io/jdsteinbach/pen/EOzWqa
但是为什么不直接使用内联高度和宽度属性呢?它们保证有效,可以针对每个图像进行设置,是语义化程度最高的,并且允许您使用 1 像素透明 png 数据 URL 作为占位符,直到图像开始加载。
“即使图像具有内联高度和宽度属性,此 CSS(作者指的是“流畅图像”样式)也会使它们在小型视窗上保持正常行为。”
你好,
数据 URI 对 SEO 有影响吗?
我只是使用 JS 根据宽度和高度属性设置填充 - 这意味着没有 JS 的客户端不会获得一个大的空白空白块(并且一个备用图像会从一个 noscript 标签加载)。
无耻的宣传:https://shakyjake.github.io/Lazy-Loading-With-IntersectionObserver/
使用
src
还有另一个重大缺陷:您正在删除图像的渐进加载/显示,这会导致图像显示得更晚。即使对于非渐进图像,这种情况也会发生,区别相当大。因此,不要使用此技术。Alex,你能详细说明一下吗?在
src
属性中提供占位符有什么缺点?每个源都有不同比率的 picture 元素怎么办?
为每个源 src 以相同的方式解决。
Picture 源不支持 src 属性,当我将内联 SVG 设置为 srcset 时,浏览器会给我这个错误:“无法解析“srcset”属性值,因为它具有未知描述符。”您建议如何将 srcset 设置为内联 SVG?谢谢。
保持纵横比是一个非常好的建议,感谢您分享这篇文章。如果目标是避免重排,那么大多数延迟加载库都做错了,因为只有 IntersectionObserver 才会避免重排。
我在这里描述了一个小型(原生)方法,也许可以启发某些人 :-) https://medium.com/@iliketoplay/lazy-loading-a-flexible-and-performant-approach-5a46b97ef60f
希望这将在不久的将来通过更好的 CSS 属性得到解决(请参见https://www.bram.us/2017/06/16/aspect-ratios-in-css-are-a-hack/ 中的“现在怎么办?”)。
文章中显示的方法无法消除重排。src 应该通过图片的加载事件进行更改。
我使用非常小的、高度压缩的缩略图(例如 40×30)来解决重排问题,这些缩略图与较大的图像(例如 800×600)具有相同的纵横比。缩略图可以内联,但像 Cloudinary 这样的服务可以动态生成它们。
然后,当需要时,完整图像会绝对定位在顶部。(然后,完整图像会变成一个标准块,缩略图会被移除 - 但这只是为了防止 Edge 中出现一些怪异的行为。)
演示:https://codepen.io/craigbuckler/pen/yPqLXW
这是一个非常巧妙的技巧!
值得注意的是,如果图像在不同屏幕尺寸下的比例不同(例如,小屏幕上为 1:1,大屏幕上为 2:1),则此技术将不起作用。浏览器只会查找
src
标签来定义元素大小,而 优化后的 URL 编码 图像不被srcset
标签接受,无法用于响应式图像。我可能错了,但这是我今天下午进行的几次测试后发现的结果。我很想看看是否有人知道解决此特定情况的方法。
更正:此技术无法与如上所述的
srcset
属性一起使用。问题在于 优化后的 URL 编码 中包含的空格,浏览器使用这些空格来分隔图像 URL 和宽度描述符。要解决此问题,您需要编写以下内容:而不是
区别在于编码的字符。文章中的字符只对
<
和>
进行了编码。对src
和srcset
属性都起作用的字符对<
、>
、"
和 **空格** 进行了编码。谢谢!
您好,这是一篇非常有趣的文章。
我只有一个问题,您使用什么工具将 SVG 转换为 base64?
谢谢
网上有很多工具可用。您甚至可以使用 Chrome DevTools 来完成此操作。
似乎 iOS 12 上的 Safari 在 SVG 占位符进入视窗时不会触发观察者事件(在 IntersectionObserver polyfill 中)。我正在研究解决方法,但目前这使得这项技术无法用于生产环境。
https://github.com/w3c/IntersectionObserver/tree/master/polyfill