采用可变像素密度的高 DPI 图片

Boris Smus
Boris Smus

发布时间:2012 年 8 月 22 日;上次更新时间:2025 年 4 月 14 日

由于市场上有如此多的设备,因此屏幕像素密度也非常多样。应用开发者需要支持一系列像素密度,这可能非常具有挑战性。在移动网络上,由于以下几种因素,这些挑战会更加复杂:

  • 各种不同规格的设备。
  • 网络带宽和电池续航时间受限。

在图片方面,Web 开发者的目标是尽可能高效地提供优质图片。本文将介绍一些目前和未来可用于实现此目的的实用技巧。

尽可能避免使用图片

在假定您需要添加图片之前,请记住,Web 上有许多强大的技术,它们在很大程度上不依赖于分辨率和 DPI。具体而言,由于 Web 在使用 devicePixelRatio 时具有自动像素放大功能,因此文本、SVG 和大部分 CSS 都能“正常运行”。

不过,您无法始终避免使用光栅图片。例如,您可能会获得一些很难用纯 SVG 或 CSS 复制的素材资源。您可能要处理的是照片。虽然您可以自动将图片转换为 SVG,但将照片矢量化没有多大意义,因为放大后的版本通常看起来不太好。

像素密度的历史

早期,计算机显示屏的像素密度为 72 或 96 每英寸点数 (DPI)

屏幕的像素密度逐渐提高,这在很大程度上是受移动设备技术进步的影响,因为用户通常会将手机靠近脸部,这会使像素更明显。到了 2008 年,150 dpi 的手机成为了新的标准。屏幕密度不断提高,如今的手机屏幕密度为 300dpi。

在实践中,低密度图片在新屏幕上的显示效果应与在旧屏幕上的显示效果相同,但与高密度用户习惯看到的清晰图片相比,低密度图片看起来会很刺眼且像素化。以下是 1x 图片在 2x 显示屏上的粗略模拟效果。相比之下,2 倍图片看起来非常不错。

1x 像素 2 倍像素
Baboon 1x 像素密度。 Baboon 2x 像素密度。
不同像素密度的狒。

网页上的像素

在设计网站时,99% 的显示屏都是 96dpi,并且很少考虑到显示屏的差异。现在,屏幕尺寸和密度差异很大,因此我们需要一种标准方法,让图片在所有屏幕上都能看起来不错。

HTML 规范通过定义制造商用于确定 CSS 像素大小的参考像素来解决此问题。

使用参考像素,制造商可以确定设备物理像素相对于标准或理想像素的大小。此比率称为设备像素比。

计算设备像素比

假设一部手机的屏幕物理像素尺寸为每英寸 180 像素 (ppi)。计算设备像素比需要完成以下三个步骤:

  1. 比较设备实际保持的距离与参考像素的距离。

    根据规范,我们知道在 28 英寸时,理想的像素密度为每英寸 96 像素。我们知道,与笔记本电脑和台式机相比,用户在使用手机时,脸部会更靠近设备。对于以下等式,我们估计该距离为 18 英寸。

  2. 将距离比率乘以标准密度 (96ppi),即可获得给定距离的理想像素密度。

    idealPixelDensity = (28/18) * 96 = 150 像素/英寸(大约)

  3. 计算物理像素密度与理想像素密度的比率,即可得出设备像素比。

    devicePixelRatio = 180/150 = 1.2

一个参考角像素,用于说明设备像素比的计算方式。

因此,现在,当浏览器需要知道如何根据理想分辨率或标准分辨率调整图片大小以适应屏幕时,浏览器会参考设备像素比 1.2。这意味着,对于每个理想像素,此设备具有 1.2 个物理像素。用于在理想像素(由 Web 规范定义)和实际像素(设备屏幕上的点)之间转换的公式如下所示:

physicalPixels = window.devicePixelRatio * idealPixels

长期以来,设备供应商倾向于对 devicePixelRatios (DPR) 进行舍入。Apple 的 iPhone 和 iPad 报告的 DPR 为 1,而其 Retina 等效设备报告的 DPR 为 2。CSS 规范建议:

像素单位是指最接近参考像素的设备像素整数。

圆形比率更好的一个原因是,它们可能会导致亚像素伪影更少。

不过,实际的设备形态要多样得多,Android 手机的 DPR 通常为 1.5。Nexus 7 平板电脑的 DPR 约为 1.33,这是通过与上例类似的计算得出的。未来将会有更多设备支持可变 DPR。因此,您绝不应假设客户端具有整数 DPR。

HiDPI 图片技术

有许多方法可以解决尽快显示最佳画质图片的问题,这些方法大致分为两类:

  1. 优化单张图片。
  2. 优化了在多张图片之间进行选择的功能。

单图方法:使用一张图片,但要对其进行巧妙的处理。 这些方法的缺点在于,您必须下载高 DPI 图片,即使在 DPI 较低的旧款设备上也是如此,因此性能必然会受到影响。以下是针对单张图片的情况的一些方法:

  • 高度压缩的 HiDPI 图片
  • 非常棒的图片格式
  • 渐进式图片格式

多图像方法:使用多张图片,但通过一些巧妙的方法来选择要加载的图片。这些方法固有开销,开发者需要创建同一素材资源的多个版本,然后确定决策策略。您有以下几种选择:

  • JavaScript
  • 服务器端提交
  • CSS 媒体查询
  • 内置浏览器功能 (image-set()<img srcset>)

高度压缩的 HiDPI 图片

在下载普通网站时,图片已经占据了 60% 的带宽。通过向所有客户端提供高 DPI 图片,我们可以提高此数值。它还能再大多少?

我运行了一些测试,生成了 1x 和 2x 的图片碎片,JPEG 质量分别为 90、50 和 20。

一张图片的六个版本,压缩率和像素密度各不相同。 一张图片的六个版本,压缩率和像素密度各不相同。 一张图片的六个版本,压缩率和像素密度各不相同。

从这小部分非科学的抽样结果来看,压缩大型图片似乎可以实现良好的质量与大小权衡。在我看来,高度压缩的 2 倍图像实际上看起来比未压缩的 1 倍图片更好。

不过,向双倍分辨率设备提供低质量的高度压缩的双倍图像,比提供画质更高的图像效果更差,而且这种做法会导致图片质量受损。如果您将 quality: 90 图片与 quality: 20 图片进行比较,会发现图片的清晰度会降低,颗粒感会增加。如果高质量图片至关重要(例如照片查看器应用),或者应用开发者不愿妥协,则可能无法接受包含 quality:20 的工件。

此比较完全使用压缩的 JPEG 图片进行。值得注意的是,广泛实现的图片格式(JPEG、PNG、GIF)之间存在许多权衡,这让我们来到了…

WebP:超棒的图片格式

WebP 是一种非常出色的图片格式,可实现出色的压缩效果,同时保持较高的图片保真度。

一种方法是使用 JavaScript 检查 WebP 支持情况。使用 data-uri 加载 1 像素的图片,等待“已加载”或“错误”事件触发,然后验证大小是否正确。Modernizr 附带有此类功能检测脚本,可与 Modernizr.webp 搭配使用。

不过,更好的方法是直接在 CSS 中使用 image() 函数。因此,如果您有 WebP 图片和 JPEG 后备图片,则可以编写以下代码:

#pic {
  background: image("foo.webp", "foo.jpg");
}

这种方法存在一些问题。首先,image() 并未广泛实现。其次,虽然 WebP 压缩技术远远优于 JPEG,但仍有相对的改进空间 - 根据此 WebP 图库,压缩后的大小大约缩减了 30%。因此,仅使用 WebP 不足以解决高 DPI 问题。

渐进式图片格式

JPEG 2000、渐进式 JPEG、渐进式 PNG 和 GIF 等渐进式图片格式有一个优势(存在争议):在图片完全加载之前,您可以看到图片显示出来。它们可能会产生一些大小开销,但关于这一点的证据并不一致。Jeff Atwood 声称,渐进式模式会“使 PNG 图片的大小增加约 20%,JPEG 和 GIF 图片的大小增加约 10%”。不过,Stoyan Stefanov 声称,对于大型文件,渐进式模式更高效(在大多数情况下)。

乍一看,在尽快提供最高质量图片的背景下,渐进式图片似乎非常有前途。其基本思想是,一旦浏览器知道额外数据不会提高图片质量(即所有保真度改进都只是像素级别的),便可以停止下载和解码图片。

虽然连接可以快速终止,但重新启动连接的开销通常很高。对于包含大量图片的网站,最有效的方法是保持单个 HTTP 连接处于活跃状态,并尽可能长时间重复使用该连接。如果由于下载了足够数量的图片而导致连接过早终止,浏览器就需要创建新的连接,这在低延迟环境中可能会非常缓慢。

解决此问题的一个方法是使用 HTTP Range 请求,让浏览器指定要提取的字节范围。智能浏览器可以发出 HEAD 请求来获取标头、对其进行处理、确定实际需要的图片大小,然后进行提取。遗憾的是,网络服务器对 HTTP Range 的支持不佳,因此这种方法不切实际。

最后,这种方法的一个明显限制是,您无法选择要加载哪张图片,只能选择同一张图片的不同保真度。因此,这无法解决“艺术指导”用例。

使用 JavaScript 确定要加载哪张图片

决定要加载哪张图片的第一种也是最明显的方法是在客户端中使用 JavaScript。通过这种方法,您可以了解有关用户代理的所有信息,并采取正确的措施。您可以使用 window.devicePixelRatio 确定设备像素比率、获取屏幕宽度和高度,甚至可能使用 navigator.connection 执行一些网络连接嗅探或发出虚假请求(如 foresight.js 库所做的那样)。收集所有这些信息后,您可以决定要加载哪张图片。

大约有 100 万个 JavaScript 库使用此技术。很遗憾,没有一个特别出色。

一个重大缺点是,您需要将图片加载延迟到预读解析器完成之后。这实际上意味着,除非 pageload 事件触发,否则图片甚至不会开始下载。如需了解详情,请参阅 Jason Grigsby 的文章

确定要在服务器上加载哪个图片

您可以为您提供的每个图片编写自定义请求处理脚本,将决策推迟到服务器端。此类处理程序会根据 User-Agent(与服务器共享的唯一信息)检查是否支持 Retina。然后,根据服务器端逻辑是否要提供高 DPI 资源,您可以加载相应的资源(根据某种已知的惯例进行命名)。

遗憾的是,User-Agent 不一定会提供足够的信息来决定设备应接收高质量还是低质量图片。此外,应避免使用 User-Agent 做出样式决策的任何解决方案。

使用 CSS 媒体查询

CSS 媒体查询是声明式的,可让您声明自己的意图,并让浏览器代表您执行正确的操作。除了媒体查询最常见的用法(匹配设备尺寸)之外,您还可以匹配 devicePixelRatio。关联的媒体查询是 device-pixel-ratio,并且具有关联的最小值和最大值变体,这可能符合您的预期。

如果您想加载高 DPI 图片,并且设备像素比率超出阈值,您可以执行以下操作:

#my-image { background: (low.png); }

@media only screen and (min-device-pixel-ratio: 1.5) {
  #my-image { background: (high.png); }
}

混入所有供应商前缀后,情况会变得稍微复杂一些,尤其是因为“min”和“max”前缀的放置位置差异很大

@media only screen and (min--moz-device-pixel-ratio: 1.5),
    (-o-min-device-pixel-ratio: 3/2),
    (-webkit-min-device-pixel-ratio: 1.5),
    (min-device-pixel-ratio: 1.5) {

  #my-image {
    background:url(high.png);
  }
}

采用这种方法,您可以重新获得 JavaScript 解决方案所不具备的预测性解析优势。此外,您还可以灵活选择自适应断点(例如,您可以使用低、中和高 DPI 图片),而服务器端方法无法做到这一点。

遗憾的是,它仍然有点难以操作,会导致 CSS 看起来很奇怪,或者需要预处理。此外,此方法仅适用于 CSS 属性,因此无法设置 <img src>,并且您的图片都必须是具有背景的元素。最后,如果仅依赖设备像素比,在 EDGE 连接的情况下,高 DPI 移动设备最终可能会下载巨大的 2x 图片素材资源。这并非最佳用户体验。

由于 image-set() 是 CSS 函数,因此无法解决 <img> 标记的问题。输入 @srcset,即可解决此问题。 下一部分将更深入地介绍 image-setsrcset

支持高 DPI 的浏览器功能

最终,您如何实现高 DPI 支持取决于您的具体要求。上述所有方法都有缺点。

现在,image-setsrcset 广受支持,是最佳解决方案。还有一些其他最佳实践,可以让我们更好地适应旧版浏览器。

这两者有何不同?image-set() 是一个 CSS 函数,适合用作 CSS 背景属性的值。srcset 是特定于 <img> 元素的属性,具有类似的语法。您可以使用这两个标记指定图片声明,但借助 srcset 属性,您还可以根据视口大小配置要加载的图片。

图片集的最佳实践

image-set() 语法接受一个或多个以英文逗号分隔的图片声明,这些声明由网址字符串或 url() 函数以及相应分辨率组成。例如:

image-set(
  url("image1.jpg") 1x,
  url("image2.jpg") 2x
);

/* You can also include image-set without `url()` */
image-set(
  "image1.jpg" 1x,
  "image2.jpg" 2x
);

这会告知浏览器有两张图片可供选择。一张图片针对 1x 显示屏进行了优化,另一张图片针对 2x 显示屏进行了优化。然后,浏览器可以根据各种因素(如果浏览器足够智能,甚至可能包括网络速度)选择要加载哪个版本。

除了加载正确的图片之外,浏览器还会相应地缩放图片。换句话说,浏览器假定 2 倍图片的大小是 1 倍图片的两倍,因此会将 2 倍图片按 2 的比例缩小,以便图片在网页上看起来大小相同。

您还可以指定特定的设备像素密度(以 DPI 为单位),而不是指定 1x、1.5x 或 Nx。

如果您担心旧版浏览器不支持 image-set 属性,可以添加回退以确保显示图片。例如:

/* Fallback support. */
background-image: url(icon1x.jpg);
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

此示例代码会在支持 image-set 的浏览器中加载适当的资源,否则会回退到 1x 资源。

至此,您可能想知道为什么不直接为 image-set() 使用 polyfill(即为其构建 JavaScript 补丁),然后就此结束?事实证明,为 CSS 函数实现高效的 polyfill 非常困难。(如需详细了解原因,请参阅这篇 www 风格讨论)。

图片 srcset

除了 image-set 提供的声明之外,srcset 元素还采用与视口大小对应的宽度和高度值,以尝试提供最相关的版本。

<img alt="my awesome image"
  src="banner.jpeg"
  srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">

在此示例中,系统会向视口宽度小于 640 像素的设备提供 banner-phone.jpeg,向小屏幕高 DPI 设备提供 banner-phone-HD.jpeg,向屏幕大于 640 像素的高 DPI 设备提供 banner-HD.jpeg,并向所有其他设备提供 banner.jpeg

为图片元素使用 image-set

您可能很想将 img 元素替换为带有背景的 <div>,并使用图片集方法。这种方法确实可行,但有一定限制。缺点是,<img> 标记具有长期的语义价值。在实践中,这对无障碍功能和网络抓取工具至关重要。

您可以使用 content CSS 属性,该属性会根据 devicePixelRation 自动缩放图片。例如:

<div id="my-content-image"
  style="content: -webkit-image-set(
    url(icon1x.jpg) 1x,
    url(icon2x.jpg) 2x);">
</div>

polyfill srcset

srcset 的一项实用功能是,它带有自然的回退机制。 如果未实现 srcset 属性,所有浏览器都知道如何处理 src 属性。此外,由于它只是一个 HTML 属性,因此可以使用 JavaScript 创建 polyfill

此 polyfill 附带单元测试,以确保其尽可能接近规范。此外,我们还设置了一些检查,以防止在原生实现 srcset 时 polyfill 执行任何代码。

总结

对于高 DPI 图片,最佳解决方案是选择 SVG 和 CSS。不过,这并不总是可行的解决方案,尤其是对于图片密集的网站。

JavaScript、CSS 和服务器端解决方案各有优劣。最有希望的方法是使用 image-setsrcset

总而言之,我的建议如下: