描述性语法

在本模块中,您将学习如何为浏览器提供图片选择,以便它可以就显示内容做出最佳决策。srcset 不是在特定断点交换图片来源的方法,也不是用于交换图片。这些语法允许浏览器独立于我们解决一个非常困难的问题:无缝请求和呈现根据用户的浏览上下文(包括视口大小、显示密度、用户偏好、带宽和无数其他因素)量身定制的图片来源。

这是一个很高的要求,肯定比我们简单地为 Web 标记图片时想要考虑的要多,并且做好它涉及的信息比我们能够访问的要多。

使用 x 描述密度

具有固定宽度的 <img> 在任何浏览上下文中都将占据视口的相同大小,而与用户显示屏的密度(构成屏幕的物理像素数)无关。例如,在原始 Google Pixel 和更新的 Pixel 6 Pro 上,固有宽度为 400px 的图片几乎占据整个浏览器视口。这两款设备都具有标准化的 412px 逻辑像素 宽视口。

然而,Pixel 6 Pro 的显示屏更清晰:6 Pro 的物理分辨率为 1440 × 3120 像素,而 Pixel 为 1080 × 1920 像素,即构成屏幕本身的硬件像素数。

设备逻辑像素和物理像素之间的比率是该显示屏的设备像素比 (DPR)。DPR 的计算方法是将设备的实际屏幕分辨率除以视口的 CSS 像素。

A DPR of 2 displayed in a console window.

因此,原始 Pixel 的 DPR 为 2.6,而 Pixel 6 Pro 的 DPR 为 3.5。

iPhone 4 是第一个 DPR 大于 1 的设备,其设备像素比为 2,屏幕的物理分辨率是逻辑分辨率的两倍。iPhone 4 之前的任何设备的 DPR 均为 1:一个逻辑像素对应一个物理像素。

如果您在 DPR 为 2 的显示屏上查看该 400px 宽的图片,则每个逻辑像素都将在显示屏的四个物理像素上呈现:两个水平像素和两个垂直像素。该图片不会从高密度显示屏中受益,它看起来与在 DPR 为 1 的显示屏上一样。当然,浏览器渲染引擎“绘制”的任何内容(例如,文本、CSS 形状或 SVG)都将绘制以适应更高密度的显示屏。但是,正如您从图片格式和压缩中学到的,栅格图是固定的像素网格。虽然可能并不总是非常明显,但与周围页面相比,放大以适应更高密度显示屏的栅格图看起来分辨率会很低。

为了防止这种放大,正在渲染的图片必须具有至少 800 像素的固有宽度。当缩小以适应布局中 400 个逻辑像素宽的空间时,该 800 像素的图片来源具有两倍的像素密度,在 DPR 为 2 的显示屏上,它看起来会非常清晰。

Close up of a flower petal showing disparity in density.

由于 DPR 为 1 的显示屏无法利用图片密度的增加,因此它将被缩小以匹配显示屏,并且正如您所知,缩小的图片看起来会很好。在低密度显示屏上,适合高密度显示屏的图片看起来会像任何其他低密度图片。

正如您在图片和性能中学到的,使用低密度显示屏的用户查看缩小到 400px 的图片来源时,只需要固有宽度为 400px 的来源。虽然更大的图片在视觉上适用于所有用户,但在小型低密度显示屏上渲染的巨大高分辨率图片来源看起来会像任何其他小型低密度图片,但感觉速度会慢得多。

您可能会猜到,DPR 为 1 的移动设备极其罕见,尽管在“桌面”浏览环境中仍然很常见。根据 Matt Hobbs 分享的数据,来自 2022 年 11 月的 GOV.UK 浏览会话中,约有 18% 的会话报告 DPR 为 1。虽然高密度图片看起来可能符合这些用户的期望,但它们会带来更高的带宽和处理成本,这尤其值得关注仍可能拥有低密度显示屏的旧款且性能较低的设备用户。

使用 srcset 可确保只有具有高分辨率显示屏的设备才能接收足够大的图片来源以使其看起来清晰,而不会将相同的带宽成本传递给具有较低分辨率显示屏的用户。

srcset 属性标识一个或多个以逗号分隔的候选项,用于渲染图片。每个候选项由两部分组成:URL(就像您在 src 中使用的那样)和描述该图片来源的语法。srcset 中的每个候选项都由其固有的宽度(“w 语法”)或预期的密度(“x 语法”)描述。

x 语法是“此来源适用于具有此密度的显示屏”的简写形式,后跟 2x 的候选项适用于 DPR 为 2 的显示屏。

<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">

支持 srcset 的浏览器将显示两个候选项:double-density.jpg2x 将其描述为适用于 DPR 为 2 的显示屏,以及 src 属性中的 low-density.jpg,如果未在 srcset 中找到更合适的候选项,则选择该候选项。对于不支持 srcset 的浏览器,该属性及其内容将被忽略,并且将像往常一样请求 src 的内容。

很容易将 srcset 属性中指定的值误认为是指令。2x 会告知浏览器关联的源文件适用于 DPR 为 2 的显示屏,这是关于来源本身的信息。它不会告诉浏览器如何使用该来源,只是告知浏览器该来源可以如何使用。这是一个细微但很重要的区别:这是一个双密度图片,而不是用于双密度显示屏的图片。

“此来源适用于 2x 显示屏”的语法与“在 2x 显示屏上使用此来源”的语法之间的差异在打印时很小,但显示密度只是浏览器用来决定要渲染的候选项的众多相互关联的因素之一,您只能知道其中一部分。例如:单独而言,您可以通过 prefers-reduced-data 媒体查询来确定用户是否启用了节省带宽的浏览器首选项,并使用该首选项始终让用户选择低密度图片,而不管其显示密度如何,但除非每个开发人员在每个网站上都一致地实施,否则对用户来说用处不大。他们可能会在一个网站上获得尊重其偏好的待遇,而在下一个网站上遇到铺天盖地的图片。

srcset/sizes 使用的有意模糊的资源选择算法为浏览器留出了空间,使其可以在带宽下降或基于最小化数据使用量的偏好来决定选择较低密度的图片,而无需我们承担关于如何、何时或在什么阈值下选择的责任。承担浏览器更适合为您处理的责任(和额外工作)是没有意义的。

使用 w 描述宽度

srcset 接受第二种类型的描述符用于图片来源候选项。它是一种功能更强大的描述符,并且对于我们的目的而言,它更容易理解。与将候选项标记为具有给定显示密度的适当尺寸不同,w 语法描述了每个候选项来源的固有宽度。同样,每个候选项都是相同的,只是尺寸不同,内容相同、裁剪方式相同且纵横比相同。但在这种情况下,您希望用户的浏览器在两个候选项之间进行选择:small.jpg(固有宽度为 600px 的来源)和 large.jpg(固有宽度为 1200px 的来源)。

srcset="small.jpg 600w, large.jpg 1200w"

这不会告诉浏览器如何处理此信息,只是向其提供用于显示图片的候选项列表。在浏览器可以决定要渲染哪个来源之前,您需要向其提供更多信息:关于图片将在页面上如何渲染的描述。为此,请使用 sizes 属性。

使用 sizes 描述用法

浏览器在传输图片方面性能非常出色。对图片资产的请求将在对样式表或 JavaScript 的请求之前很久启动,通常甚至在标记完全解析之前就启动。当浏览器发出这些请求时,除了标记之外,它没有关于页面本身的任何信息,它甚至可能尚未启动对外部样式表的请求,更不用说应用它们了。当浏览器解析您的标记并开始发出外部请求时,它只有浏览器级别的信息:用户的视口大小、用户显示屏的像素密度、用户偏好等等。

这没有告诉我们任何关于图片在页面布局中应如何渲染的信息,它甚至不能使用视口作为 img 大小上限的代理,因为它可能占据水平滚动的容器。因此,我们需要向浏览器提供此信息,并使用标记来完成。这就是我们能够用于这些请求的所有内容。

srcset 类似,sizes 旨在在解析标记后立即提供关于图片的信息。正如 srcset 属性是“这是源文件及其固有大小”的简写形式一样,sizes 属性是“这是渲染图片在布局中的大小”的简写形式。您描述图片的方式是相对于视口的,再次强调,视口大小是浏览器在发出图片请求时拥有的唯一布局信息。

这在打印时听起来可能有点复杂,但在实践中更容易理解

<img
 sizes="80vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">

在这里,此 sizes 值告知浏览器,我们的布局中 img 占据的空间宽度为 80vw,即视口的 80%。请记住,这不是指令,而是对图片在页面布局中大小的描述。它不是说“使此图片占据视口的 80%”,而是说“一旦页面渲染完成,此图片最终将占据视口的 80%”。

作为开发人员,您的工作已完成。您已在 srcset 中准确描述了候选项来源列表,并在 sizes 中描述了图片的宽度,并且与 srcset 中的 x 语法一样,其余的都由浏览器决定。

但为了充分理解如何使用此信息,让我们花一点时间来了解用户的浏览器在遇到此标记时做出的决策

您已告知浏览器此图片将占据可用视口的 80%,因此,如果我们在具有 1000 像素宽视口的设备上渲染此 img,则此图片将占据 800 像素。然后,浏览器将采用该值,并将其除以我们在 srcset 中指定的每个图片来源候选项的宽度。最小的来源的固有大小为 600 像素,因此:600÷800=0.75。我们的中等图片宽度为 1200 像素:1200÷800=1.5。我们最大的图片宽度为 2000 像素:2000÷800=2.5。

这些计算的结果(0.751.52.5)实际上是专门为用户的视口大小量身定制的 DPR 选项。由于浏览器还掌握了有关用户显示密度的信息,因此它会做出一系列决策

在此视口大小下,无论用户的显示密度如何,small.jpg 候选项都会被丢弃,因为计算出的 DPR 低于 1,因此此来源需要为任何用户进行放大,因此不合适。在 DPR 为 1 的设备上,medium.jpg 提供了最接近的匹配,该来源适用于在 DPR 为 1.5 的情况下显示,因此它比必要的稍微大一些,但请记住,缩小是一个视觉上无缝的过程。在 DPR 为 2 的设备上,large.jpg 是最接近的匹配,因此它被选中。

如果在 600 像素宽的视口上渲染同一张图片,则所有这些计算的结果将完全不同:80vw 现在是 480px。当我们用它除以我们的来源宽度时,我们得到 1.252.54.1666666667。在此视口大小下,在 1x 设备上将选择 small.jpg,在 2x 设备上将匹配 medium.jpg

此图片在所有这些浏览上下文中看起来都相同:我们所有的源文件除了尺寸外都完全相同,并且每个文件都以用户显示密度允许的最大清晰度渲染。但是,为了适应最大的视口和最高的密度显示屏,而不是向每个用户提供 large.jpg,用户将始终获得最小的合适候选项。通过使用描述性语法而不是规定性语法,您无需手动设置断点并考虑未来的视口和 DPR,只需向浏览器提供信息并允许它为您确定答案即可。

由于我们的 sizes 值与视口相关,并且完全独立于页面布局,因此它增加了一层复杂性。很少有图片占据视口百分比的情况,而没有任何固定宽度的边距、内边距或页面上其他元素的影响。您经常需要使用单位组合来表示图片的宽度:百分比、empx 等。

幸运的是,您可以在此处使用 calc(),任何原生支持自适应图片的浏览器也将支持 calc(),从而允许我们混合和匹配 CSS 单位,例如,占据用户视口全宽的图片,两侧减去 1em 的边距

<img
    sizes="calc(100vw-2em)"
    srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
    src="fallback.jpg"
    alt="...">

描述断点

如果您花费了大量时间处理自适应布局,您可能已经注意到这些示例中缺少的内容:图片在布局中占据的空间很可能在我们的布局断点之间发生变化。在这种情况下,您需要向浏览器传递更多细节:sizes 接受一组以逗号分隔的渲染图片大小的候选项,就像 srcset 接受以逗号分隔的图片来源候选项一样。这些条件使用熟悉的媒体查询语法。此语法是先匹配:一旦媒体条件匹配,浏览器就会停止解析 sizes 属性,并应用指定的值。

假设您有一张图片旨在占据视口的 80%,两侧减去一个 em 的内边距,在 1200px 以上的视口上,在较小的视口上,它占据视口的全部宽度。

  <img
     sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

如果用户的视口大于 1200px,则 calc(80vw - 2em) 描述了图片在我们的布局中的宽度。如果 (min-width: 1200px) 条件匹配,则浏览器会继续处理下一个值。由于没有与此值关联的特定媒体条件,因此 100vw 用作默认值。如果您要使用 max-width 媒体查询编写此 sizes 属性

  <img
     sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

用通俗易懂的语言来说:“(max-width: 1200px) 是否匹配?如果不匹配,则继续。下一个值 calc(80vw - 2em) 没有限定条件,因此这是选择的值。”

现在,您已向浏览器提供了有关 img 元素的所有信息(潜在来源、固有宽度以及您打算如何向用户呈现图片),浏览器使用一组模糊的规则来确定如何处理该信息。如果这听起来很模糊,那么好吧,那是因为它确实如此,这是故意的。HTML 规范中编码的来源选择算法明确地模糊了应如何选择来源。一旦解析了来源、其描述符以及图片的渲染方式,浏览器就可以自由地执行它想做的任何事情,您无法确定浏览器将选择哪个来源。

“在高分辨率显示屏上使用此来源”的语法是可预测的,但它不会解决自适应布局中图片的核心问题:节省用户带宽。屏幕的像素密度仅与互联网连接速度略有相关,甚至根本不相关。如果您使用的是顶级笔记本电脑,但通过计量连接、通过手机共享网络或使用不稳定的飞机 Wi-Fi 连接浏览网络,您可能希望选择退出高分辨率图片来源,而不管您的显示屏质量如何。

将最终决定权留给浏览器,可以实现比我们使用严格的规定性语法所能管理的性能改进要多得多的改进。例如:在大多数浏览器中,使用 srcsetsizes 语法的 img 永远不会费心请求尺寸小于用户浏览器缓存中已有的来源的来源。当浏览器可以无缝缩小它已有的图片来源时,对看起来相同的来源发出新请求有什么意义呢?但是,如果用户将其视口放大到需要新图片以避免放大的程度,则仍然会发出该请求,因此一切看起来都符合您的预期。

缺乏显式控制听起来可能有点可怕,但由于您使用的是内容相同的源文件,因此无论浏览器做出什么决策,我们向用户呈现“损坏”体验的可能性都不会比使用单来源 src 更高。

使用 sizessrcset

这是一个大量的信息,无论对于您(读者)还是对于浏览器而言。srcsetsizes 都是密集的语法,用相对较少的字符描述了令人震惊的信息量。也就是说,无论好坏,这都是故意的:使这些语法不那么简洁,并且更容易被我们人类解析,可能会使浏览器更难解析它们。添加到字符串中的复杂性越高,解析器错误或不同浏览器之间行为意外差异的可能性就越大。但是,这里有一个好处:机器更容易读取的语法也更容易被机器编写。

srcset 是自动化的明确案例。您很少会为生产环境手工制作图片的多个版本,而是使用 Gulp 等任务运行程序、Webpack 等捆绑器、Cloudinary 等第三方 CDN 或已内置到您选择的 CMS 中的功能来自动化该过程。如果有足够的信息首先生成我们的来源,则系统将有足够的信息将其写入可行的 srcset 属性。

sizes 的自动化难度稍大。如您所知,系统可以计算渲染布局中图片大小的唯一方法是渲染布局。幸运的是,许多开发者工具已经涌现出来,以抽象出手工编写 sizes 属性的过程,其效率是您手工永远无法比拟的。respImageLint 例如,是一段旨在审查您的 sizes 属性的准确性并提供改进建议的代码片段。Lazysizes 项目通过在布局建立后推迟图片请求来牺牲一些速度以换取效率,从而允许 JavaScript 为您生成 sizes 值。如果您使用的是完全客户端渲染框架(例如 React 或 Vue),则有许多用于创作和/或生成 srcsetsizes 属性的解决方案,我们将在CMS 和框架中进一步讨论。