当今复杂的设备形态的一个特点是,屏幕像素密度非常多样。有些设备具有非常高的显示分辨率,而有些设备则落后。应用开发者需要支持一系列像素密度,这可能非常具有挑战性。在移动网络上,由于以下几种因素,这些挑战会更加复杂:
- 各种不同外形规格的设备。
- 网络带宽和电池续航时间受限。
在图片方面,Web 应用开发者的目标是尽可能高效地提供优质图片。本文将介绍目前和近期可用于实现此目的的一些实用技巧。
尽可能避免使用图片
在打开这个潘多拉魔盒之前,请记住,Web 上有许多强大的技术,它们在很大程度上不依赖于分辨率和 DPI。具体而言,由于 Web 的自动像素放大功能(通过 devicePixelRatio),文本、SVG 和大部分 CSS 将“正常运行”。
不过,您无法始终避免使用光栅图片。例如,您可能会获得很难用纯 SVG/CSS 复制的素材资源,或者您要处理的是照片。虽然您可以自动将图片转换为 SVG,但将照片矢量化没有多大意义,因为放大后的版本通常看起来不太好。
背景
显示密度简史
早期,计算机显示屏的像素密度为 72 或 96dpi(每英寸的点数)。
显示屏的像素密度逐渐提高,这在很大程度上是受移动设备用例的影响,在这种用例中,用户通常会将手机靠近脸部,因此像素更容易被看到。到了 2008 年,150 dpi 的手机成为了新的标准。显示密度不断提高的趋势仍在延续,如今的新手机采用 300 dpi 显示屏(Apple 将其命名为“Retina”)。
当然,理想状态是像素完全不可见。对于手机外形规格,当前一代 Retina/HiDPI 显示屏可能接近理想状态。不过,Project Glass 等新类硬件和穿戴式设备可能会继续推动像素密度不断提高。
在实践中,低密度图片在新屏幕上的显示效果应与在旧屏幕上的显示效果相同,但与高密度用户习惯看到的清晰图片相比,低密度图片看起来会很刺眼且像素化。以下是 1x 图片在 2x 显示屏上的粗略模拟效果。相比之下,2 倍图片看起来非常不错。
网页上的像素
在设计 Web 时,99% 的显示屏都是 96dpi(或假装是),并且很少考虑到在这方面存在差异。由于屏幕尺寸和密度差异很大,因此我们需要一种标准方法,让图片在各种屏幕密度和尺寸下都能看起来不错。
HTML 规范最近通过定义制造商用来确定 CSS 像素大小的参考像素来解决此问题。
使用参考像素,制造商可以确定设备物理像素相对于标准或理想像素的大小。此比率称为设备像素比。
计算设备像素比
假设一部智能手机的屏幕物理像素尺寸为每英寸 180 像素 (ppi)。计算设备像素比需要完成以下三个步骤:
比较设备实际持握距离与参考像素的距离。
根据规范,我们知道在 28 英寸时,理想的每英寸像素数为 96。不过,由于它是智能手机,因此用户会将设备握得比笔记本电脑更靠近脸部。我们假设该距离为 18 英寸。
将距离比率乘以标准密度 (96ppi),即可获得给定距离的理想像素密度。
idealPixelDensity = (28/18) * 96 = 150 像素/英寸(大约)
计算物理像素密度与理想像素密度的比率,即可得出设备像素比。
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 为整数。
高 DPI 图片技术概览
有许多方法可以解决尽快显示最佳画质图片的问题,这些方法大致分为两类:
- 优化单张图片,以及
- 优化了在多张图片之间进行选择的功能。
单图方法:使用一张图片,但要对其进行巧妙的处理。 这些方法的缺点在于,您不可避免地会牺牲性能,因为即使在 DPI 较低的旧版设备上,您也需要下载高 DPI 图片。以下是针对单张图片的情况的一些方法:
- 高度压缩的 HiDPI 图片
- 非常棒的图片格式
- 渐进式图片格式
多图像方法:使用多张图片,但通过一些巧妙的方法来选择要加载的图片。这些方法固有开销,开发者需要创建同一资源的多个版本,然后确定决策策略。您有以下几种选择:
- JavaScript
- 服务器端提交
- CSS 媒体查询
- 内置浏览器功能 (
image-set()
、<img srcset>
)
高度压缩的 HiDPI 图片
在下载一个普通网站时,图像已经占据了 60% 的带宽。通过向所有客户端提供高 DPI 图片,我们将提高这一比例。它还会变大多少?
我进行了一些测试,生成了 1x 和 2x 的图片碎片,JPEG 质量分别为 90、50 和 20。下面是我用来生成这些图片的 Shell 脚本(采用 ImageMagick):
从这小部分非科学的样本来看,压缩大型图片似乎可以实现良好的质量与大小权衡。在我看来,高度压缩的 2 倍图像实际上比未压缩的 1 倍图片看起来更好。
当然,向双倍分辨率设备提供低质量、高度压缩的双倍分辨率图片,比提供更高质量的图片更糟糕,而且上述方法会导致图片质量下降。如果您将画质为 90 的图片与画质为 20 的图片进行比较,您会发现清晰度下降且颗粒感增加。如果高质量图片至关重要(例如照片查看器应用),或者应用开发者不愿意妥协,则可能无法接受这些工件。
以上比较完全使用压缩的 JPEG 图片进行的。值得注意的是,广泛实现的图片格式(JPEG、PNG、GIF)之间存在许多权衡,这让我们来到了…
非常棒的图片格式
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 库可执行上述操作,但遗憾的是,其中没有一个特别出色。
此方法的一大缺陷是,使用 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);
}
}
通过这种方法,您可以重新获得 JS 解决方案丢失的预测解析优势。此外,您还可以灵活选择自适应断点(例如,您可以使用低、中和高 DPI 图片),而服务器端方法无法做到这一点。
遗憾的是,它仍然有点难以操作,并且会导致 CSS 看起来很奇怪(或需要预处理)。此外,此方法仅适用于 CSS 属性,因此无法设置 <img src>
,并且您的图片都必须是具有背景的元素。最后,如果仅依赖设备像素比,在某些情况下,高 DPI 智能手机在连接到 EDGE 网络时,可能会下载巨大的 2x 图片素材资源。这并不是最佳用户体验。
使用新的浏览器功能
最近,我们就 Web 平台对高 DPI 图片问题的支持进行了大量讨论。Apple 最近也加入了这一领域,将 image-set() CSS 函数引入了 WebKit。因此,Safari 和 Chrome 都支持它。由于它是 CSS 函数,因此 image-set()
无法解决 <img>
代码的问题。引入 @srcset 可解决此问题,但(在撰写本文时)尚无参考实现!下一部分将深入介绍 image-set
和 srcset
。
支持高 DPI 的浏览器功能
最终,您采用哪种方法取决于您的具体要求。不过,请注意,上述所有方法都有缺点。不过,未来,一旦 image-set
和 srcset 获得广泛支持,它们将成为解决此问题的合适方案。暂时先来聊聊一些最佳实践,它们有助于我们尽可能接近理想的未来。
首先,这两者有何不同?image-set()
是一个 CSS 函数,适合用作 CSS 背景属性的值。srcset 是特定于 <img>
元素的属性,语法类似。您可以使用这两个标记指定图片声明,但通过 srcset 属性,您还可以根据视口大小配置要加载的图片。
图片集最佳实践
image-set()
CSS 函数的前缀为 -webkit-image-set()
。语法非常简单,接受一个或多个以英文逗号分隔的图片声明,其中包含网址字符串或 url()
函数,后跟关联的分辨率。例如:
background-image: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
这会告知浏览器有两张图片可供选择。其中一个针对 1x 显示屏进行了优化,另一个针对 2x 显示屏进行了优化。然后,浏览器可以根据各种因素(如果浏览器足够智能,甚至可能包括网络速度)选择要加载哪个版本(据我所知,目前尚未实现)。
除了加载合适的图像外,浏览器也会相应地调整其大小。换句话说,浏览器假设 2x 图像是 1x 图像的两倍大,因此会将 2x 图像缩小一半,最后图像在页面上看上去就一样大。
您还可以指定特定的设备像素密度(以 dpi 为单位),而不是指定 1x、1.5x 或 Nx。
这种方法非常有效,但在不支持 image-set
属性的浏览器中,系统将完全不显示图片!这显然很糟糕,因此您必须使用回退(或一系列回退)来解决此问题:
background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
Also include other prefixed versions of this */
background-image: image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
上例中,会在支持 image-set 的浏览器中加载合适的资产,否则将改为加载 1x 资产。需要特别注意的是,支持 image-set()
的浏览器非常少,大部分用户代理会加载 1x 资产。
此演示使用 image-set()
加载正确的图片,如果不支持此 CSS 函数,则回退到 1x 资源。
至此,您可能想知道为什么不直接为 image-set()
使用 polyfill(即为其构建 JavaScript 补丁),然后就此结束?事实证明,为 CSS 函数实现高效的 polyfill 非常困难。(如需详细了解原因,请参阅这篇 www 风格讨论)。
图片 srcset
下面是 srcset 的示例:
<img alt="my awesome image"
src="banner.jpeg"
srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">
如您所见,除了 image-set
提供的 x 声明之外,srcset 元素还采用与视口大小对应的 w 和 h 值,以尝试提供最相关的版本。上述代码会将 banner-phone.jpeg 提供给视口宽度小于 640 像素的设备,将 banner-phone-HD.jpeg 提供给小屏幕高 DPI 设备,将 banner-HD.jpeg 提供给屏幕大于 640 像素的高 DPI 设备,并将 banner.jpeg 提供给所有其他设备。
为图片元素使用 image-set
由于大多数浏览器都未实现 img 元素上的 srcset 属性,因此您可能很想将 img 元素替换为带有背景的 <div>
,并使用图片集方法。这种方法可以使用,但有一定限制。缺点是,<img>
标记具有长期的语义价值。在实践中,这主要出于网页抓取工具和无障碍功能方面的考虑。
如果您最终使用 -webkit-image-set
,可能会想使用 background CSS 属性。此方法的缺点是,您需要指定图片大小,如果您使用的是非 1x 图片,则大小是未知的。您可以改用 content CSS 属性,如下所示:
<div id="my-content-image"
style="content: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x);">
</div>
这会根据 devicePixelRatio 自动缩放图片。如需查看上述技术的实际运作方式,请参阅此示例,其中针对不支持 image-set
的浏览器额外回退到 url()
。
为 srcset 添加 polyfill
srcset
的一项实用功能是,它附带自然的回退机制。
如果未实现 srcset 属性,所有浏览器都知道如何处理 src 属性。此外,由于它只是一个 HTML 属性,因此可以使用 JavaScript 创建 polyfill。
此 polyfill 附带单元测试,以确保其尽可能贴近规范。此外,我们还设置了一些检查,以防止在 srcset 以原生方式实现时,polyfill 执行任何代码。
下面是 polyfill 演示。
总结
没有任何灵丹妙药可以解决高 DPI 图片问题。
最简单的解决方案是完全避免使用图片,改用 SVG 和 CSS。不过,这并不总是现实情况,尤其是当您的网站上有高质量图片时。
JS、CSS 和服务器端方法各有优缺点。不过,最有希望的方法是利用新的浏览器功能。虽然浏览器对 image-set
和 srcset
的支持仍不完善,但目前有合理的回退方案可供使用。
总而言之,我的建议如下:
- 对于背景图片,请为不支持 image-set 的浏览器使用 image-set 和适当的回退方案。
- 对于内容图片,请使用 srcset polyfill,或回退到使用 image-set(见上文)。
- 如果您愿意牺牲图片质量,不妨考虑使用经过大幅压缩的 2 倍图片。