要预测响应式图像候选项,请将 sizes 解析为 CSS 宽度,乘以设备像素比,然后将该目标值与 srcset 比较。接着验证所选 URL:浏览器最终仍可自行决定。

本指南适用于在 Mac 上用 Chrome 检查自己编写的网页的人。你将计算可能的候选项,测试一个包含原创抽象图形的本地页面,并区分误导性的尺寸声明与正常的浏览器选择行为。

sizes、srcset 和 DPR 分别向浏览器提供什么信息?

使用宽度描述符时,srcset 会列出可用资源及其固有像素宽度。像 800w 这样的描述符必须对应实际宽度为 800 像素的图像。请将宽度描述符与 2x 这样的密度描述符放在不同的源集里;在同一个 srcset 中混用是无效的。MDN 的图像元素参考说明了这些要求。

sizes 为资源选择提供预期的显示宽度。浏览器按顺序评估其中的条件,并使用第一个匹配项。请将无条件的回退项放在最后。源尺寸支持 480px、50vw 和 calc(100vw - 32px) 这样的长度;50% 这样的百分比值不是有效的槽位宽度。在 HTML 中使用响应式图像

CSS 控制显示尺寸。请将 CSS 和显式的 sizes 列表视为两种必须保持一致的描述:一种决定布局,另一种告知资源选择。只更改 sizes 并不会实现相应的布局。HTML 标准:sizes 属性

DPR 描述物理像素与 CSS 像素之间的关系。请读取 window.devicePixelRatio,不要根据 Mac 型号或显示器名称推断数值。页面缩放可能改变该值,在不同显示器之间移动窗口也可能改变它。捏合缩放不会改变 DPR。MDN:Window.devicePixelRatio

如何计算可能的候选项?

对于已解析的源尺寸 S、资源宽度 W 和 DPR D,可以使用以下两种等价的计算方法:

目标资源宽度 = S × D
候选项有效密度 = W ÷ S

HTML 处理模型通过将每个候选宽度除以源尺寸来规范化宽度描述符。MDN 也说明了实用的预测方法:将尺寸提示乘以 DPR。HTML 标准:规范化源密度,MDN:HTMLImageElement.sizes

作为实用的估算方法,请选择大于或等于目标宽度的最小候选项。如果所有候选项都低于目标值,请指出可用的最大候选项并记录差值。下面的练习明确采用了这条预测规则。HTML 标准将最终选择留给具体实现,并未要求所有浏览器都遵循这条取整规则。HTML 标准:选择图像源

请看以下源尺寸列表,以及 480w、800w、1200w 和 1440w 这几个候选项:

(max-width: 600px) calc(100vw - 32px),
(max-width: 1200px) calc(50vw - 32px),
568px

100vw 表示视口宽度,50vw 表示视口宽度的一半。这些单位指的是视口,而不是图像的父容器。CSS 值与单位:视口百分比长度

下表中的计算仅用于说明,并非记录的浏览器结果。最后一列采用了上述预测规则。

视口宽度,CSS 像素 DPR 已解析的源尺寸,CSS 像素 目标资源宽度,像素 预测候选项
600 1 568 568 800w
600 2 568 1,136 1200w
601 2 268.5 537 800w
1,000 1 468 468 480w
1,000 2 468 936 1200w
1,400 2 568 1,136 1200w
1,400 3 568 1,704 1440w,低于目标值

从 600 到 601 CSS 像素的变化特别值得测试。在 600 像素时,第一个条件提供接近全宽的槽位。在 601 像素时,第二个条件提供窄得多的槽位。因此,视口变宽反而会产生更小的目标图像。这是由页面编写时设定的布局约定直接决定的。

当视口宽度为 1,000 像素、DPR 为 2 时,源尺寸为 468 CSS 像素。四个候选项的有效密度约为 1.03x、1.71x、2.56x 和 3.08x。按照我们的预测规则,1200w 候选项是第一个超过目标密度 2x 的项。

如何构建完整的本地测试?

请使用安装了 Chrome 和 Python 3.8 或更高版本的 Mac。在纯文本编辑器中将以下完整程序保存为 responsive_demo.py。程序会生成四个 PNG 文件,其中包含相同的矩形和圆形,写入一个匹配的页面,并启动本地服务器。所有生成的文件都会写入新的临时目录,因此不会覆盖现有文件。

该页面特意使用了立即加载的图像和显式源尺寸。其 CSS 宽度与计算表中的三个分支相匹配。服务器使用 Python 文档所述的 ThreadingHTTPServer 和基于目录的请求处理器。Python:http.server

from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import struct
import tempfile
import zlib


def chunk(kind, data):
    return (
        struct.pack("!I", len(data)) + kind + data
        + struct.pack("!I", zlib.crc32(kind + data))
    )


def make_png(width):
    height = width // 2
    rows = []
    for y in range(height):
        row = bytearray()
        for x in range(width):
            color = (242, 244, 248)
            if width // 8 <= x < width // 2 and height // 4 <= y < 3 * height // 4:
                color = (70, 100, 160)
            if (x - 3 * width // 4) ** 2 + (y - height // 2) ** 2 < (height // 5) ** 2:
                color = (220, 160, 60)
            row.extend(color)
        rows.append(b"\x00" + row)
    header = struct.pack("!IIBBBBB", width, height, 8, 2, 0, 0, 0)
    return (
        b"\x89PNG\r\n\x1a\n"
        + chunk(b"IHDR", header)
        + chunk(b"IDAT", zlib.compress(b"".join(rows)))
        + chunk(b"IEND", b"")
    )


page = '''<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shape A candidate test</title>
<style>
  body { margin: 0; padding: 16px; font-family: sans-serif; }
  #shape { display: block; width: calc(100vw - 32px); height: auto; }
  @media (min-width: 600.01px) {
    #shape { width: calc(50vw - 32px); }
  }
  @media (min-width: 1200.01px) {
    #shape { width: 568px; }
  }
</style>
<p>Shape A: rectangle and circle</p>
<img id="shape" src="shape-a-800.png" width="1440" height="720"
     alt="Blue rectangle and amber circle on a pale background"
     srcset="shape-a-480.png 480w, shape-a-800.png 800w,
             shape-a-1200.png 1200w, shape-a-1440.png 1440w"
     sizes="(max-width: 600px) calc(100vw - 32px),
            (max-width: 1200px) calc(50vw - 32px), 568px">
</html>
'''

root = Path(tempfile.mkdtemp(prefix="responsive-shapes-"))
for width in (480, 800, 1200, 1440):
    (root / f"shape-a-{width}.png").write_bytes(make_png(width))
(root / "index.html").write_text(page, encoding="utf-8")
handler = partial(SimpleHTTPRequestHandler, directory=str(root))
with ThreadingHTTPServer(("127.0.0.1", 0), handler) as server:
    print(f"Files: {root}", flush=True)
    print(f"Open http://127.0.0.1:{server.server_port}/", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass

在终端中切换到保存程序的文件夹,然后运行:

python3 responsive_demo.py

在 Chrome 中打开程序打印的本地地址。测试期间请保持终端运行;完成后在终端中按 Control+C。程序会请求一个可用端口,因此请使用它打印的地址,不要假定端口号固定。

对于桌面测试的整数视口宽度,CSS 阈值使用 600.01px 和 1200.01px。请测试表格中的确切整数值。将此示例用于生产布局时,请使实际 CSS 断点与源尺寸条件保持一致,包括任何小数宽度行为。

如何在 Mac 上的 Chrome 中验证选择结果?

右键单击图形并选择 Inspect,打开 DevTools 并选中其图像元素。Chrome 将此方法作为直接检查元素及其所应用样式的方式。打开 Chrome DevTools

  1. 点击 Toggle device toolbar,然后在 Dimensions 下选择 Responsive。
  2. 将视口宽度设为 1000。
  3. 在工具栏的 More options 菜单中选择 Add device pixel ratio,然后选择 DPR 2。
  4. 如有需要,选择 More options,然后选择 Add device type,再选 Desktop。
  5. 打开 Network,启用 Disable cache,并在 DevTools 打开时重新加载页面。
  6. 通过筛选 shape-a 查找请求的图像。

Chrome 在其设备模式指南中说明了视口、DPR 和设备类型控件,并在其网络功能参考中说明了缓存控件。

页面加载后,在 DevTools 的 Console 中运行以下完整诊断代码片段:

(() => {
  const image = document.getElementById("shape");
  if (!image) throw new Error("The Shape A image was not found.");

  const viewport = window.innerWidth;
  const slot = window.matchMedia("(max-width: 600px)").matches
    ? viewport - 32
    : window.matchMedia("(max-width: 1200px)").matches
      ? viewport / 2 - 32
      : 568;

  console.table({
    viewportCSSPixels: viewport,
    devicePixelRatio: window.devicePixelRatio,
    declaredSlotCSSPixels: slot,
    renderedWidthCSSPixels: image.getBoundingClientRect().width,
    targetResourcePixels: slot * window.devicePixelRatio,
    selectedURL: image.currentSrc,
    naturalWidthCSSPixels: image.naturalWidth
  });
})();

此代码片段专门为这个演示计算声明的槽位;它不是通用的 sizes 解析器。将声明的槽位与渲染宽度比较,再将所选文件名与预测表比较。

currentSrc 会报告浏览器所选的完整 URL。它不能证明资源已成功加载,因此还要检查图像请求和显示结果。读取 src 只能查看本例中的回退属性。MDN:HTMLImageElement.currentSrc

不要将 naturalWidth 误认为 PNG 编码后的宽度。它是经过密度校正的 CSS 像素宽度。在此受控测试中,可通过生成文件的尺寸和文件名确定其资源宽度。MDN:HTMLImageElement.naturalWidth

在 600 和 601 CSS 像素下重复测试,并在每种条件下重新加载。记录视口、报告的 DPR、槽位、所选文件名以及请求是否完成。这些记录可以区分重复的新加载结果与已加载页面在调整大小时做出的一系列选择。

预测结果不同时应检查什么?

在更改候选列表之前,先逐项检查输入:

  1. 检查描述符类型。 使用 1x 和 2x 这样的密度描述符时,不要通过 sizes 应用宽度描述符的计算方法。
  2. 检查第一个匹配条件。 较早匹配的条件优先于后面的项。
  3. 比较声明宽度与渲染宽度。 100vw 声明可能会高估被 CSS 限制在窄列中的槽位宽度。
  4. 读取实际 DPR。 页面缩放或更换显示器后重新检查。
  5. 比较受控的重新加载结果。 重复测试时,保持视口、DPR 和加载条件相同。
  6. 检查资源是否可用。 URL 被选中与图像成功显示是两项不同的观察结果。

显式源尺寸缺失或不可用时,HTML 解析器可能会采用其 100vw 默认值。这可以解释窄图像为何会有出乎意料的大目标值。请提供准确的源尺寸列表,不要假定浏览器会根据 CSS 推断预期的列宽。HTML 标准:解析 sizes 属性

如果页面使用了 picture 元素,请先确定适用的源分支,再计算密度。选择源分支后,才会从该分支的候选项中进行选择;不同裁切属于这种艺术指导机制。在 HTML 中使用响应式图像

哪些支持限制会影响这种方法?

MDN 标记 sizes 属性为各浏览器广泛支持,但这并不表示每个较新的关键字都同样受支持。这里使用的显式条件可以直接用于测试,无需依赖自动尺寸调整。MDN:HTMLImageElement.sizes

sizes="auto" 对延迟加载的图像有效,并允许选择时使用其布局宽度。如果采用它,请在 auto 之后保留显式回退尺寸,以供不支持此关键字的浏览器使用,并提供图像尺寸。若不调整这些前提条件,请勿将它替换到此立即加载示例中。MDN 的图像元素参考

Chrome 的设备控件只能在模拟条件下检验 Chrome 的行为,无法证明 Safari 或 Firefox 会选择哪些候选项。Chrome 还说明设备模式只是近似模拟,因此如果其他浏览器的行为很重要,请直接在该浏览器中测试。Chrome DevTools:设备模式的局限性

一个实用的预测应以可核查的记录收尾:适用的源集、已解析的源尺寸、报告的 DPR、计算出的目标值,以及观察到的 currentSrc。当这些输入与编写时设定的布局一致时,你就能清楚解释资源选择,并找出下一步应修改 CSS、sizes 还是可用的图像宽度。

来源