画布中实际有多少像素?
自 Chrome 84 起,ResizeObserver 支持一种新的盒子测量方式 devicePixelContentBox
,用于以物理像素为单位测量元素的尺寸。这有助于渲染像素级精准的图形,尤其是在高密度屏幕上。
背景:CSS 像素、画布像素和物理像素
虽然我们经常使用抽象的长度单位(例如 em
、%
或 vh
),但最终都归结为像素。每当我们在 CSS 中指定元素的大小或位置时,浏览器的布局引擎最终都会将该值转换为像素 (px
)。这些是“CSS 像素”,它们具有悠久的历史,并且与屏幕上的像素只有松散的关系。
在很长一段时间内,使用 96DPI(每英寸的点数)来估计任何人的屏幕像素密度都是相当合理的,这意味着任何给定的显示器每厘米大约有 38 个像素。随着时间的推移,显示器尺寸变大和/或变小,或者开始在相同的表面积上显示更多像素。再加上网页上的许多内容都以 px
为单位定义其尺寸(包括字体大小),最终导致这些高密度 (“HiDPI”) 屏幕上的文字难以辨认。作为一种对策,浏览器会隐藏显示器的实际像素密度,而是假装用户使用的是 96 DPI 的显示屏。CSS 中的 px
单位表示此虚拟 96 DPI 显示屏上一个像素的大小,因此称为“CSS 像素”。此单位仅用于测量和定位。在进行任何实际渲染之前,系统会先转换为物理像素。
如何从这个虚拟显示屏过渡到用户的实际显示屏?输入 devicePixelRatio
。此全局值用于告知您需要多少个物理像素才能形成一个 CSS 像素。如果 devicePixelRatio
(设备像素比)为 1
,则您使用的是大约 96DPI 的显示器。如果您使用的是 Retina 屏幕,则 dPR 可能为 2
。在手机上,遇到更高的(也更奇怪的)dPR 值(例如 2
、3
甚至 2.65
)并不罕见。请务必注意,此值是确切值,但无法由此推导出显示器的实际 DPI 值。dPR 为 2
表示 1 个 CSS 像素将精确映射到 2 个物理像素。
1
…它的宽度为 3440 像素,显示区域宽度为 79 厘米。
这会带来 110 DPI 的分辨率。接近 96,但不是。
这也是为什么在大多数显示屏上,<div style="width: 1cm; height: 1cm">
的大小不会正好是 1 厘米的原因。
最后,dPR 还可能会受到浏览器缩放功能的影响。如果您放大,浏览器会增加报告的 dPR,从而使所有内容都以更大的尺寸呈现。如果您在缩放时在开发者工具控制台中检查 devicePixelRatio
,则会看到显示的小数值。

devicePixelRatio
。我们来添加 <canvas>
元素。您可以使用 width
和 height
属性指定画布所需的像素数。因此,<canvas width=40 height=30>
将是一个 40x30 像素的画布。不过,这并不意味着它会以 40x30 像素的尺寸显示。默认情况下,画布将使用 width
和 height
属性来定义其固有大小,但您可以使用熟悉且喜爱的所有 CSS 属性来任意调整画布的大小。根据我们目前所学的内容,您可能会意识到,这种方法并非在所有情况下都是理想的。画布上的一个像素最终可能会覆盖多个物理像素,或者仅覆盖一部分物理像素。这可能会导致令人不悦的视觉伪影。
总结来说:画布元素具有指定的大小,用于定义可供您绘制的区域。画布像素数完全独立于以 CSS 像素为单位指定的画布显示尺寸。CSS 像素的数量与物理像素的数量不同。
Pixel 完美体验
在某些情况下,最好能实现画布像素与物理像素之间的精确映射。如果实现了这种映射,则称为“像素级完美”。像素级精确渲染对于清晰地渲染文字至关重要,尤其是在使用子像素渲染或显示亮度交替的紧密对齐线条的图形时。
为了在 Web 上实现尽可能接近像素级完美的画布,我们或多或少会采用以下方法:
<style>
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
// Get the canvas' size in CSS pixels
const rectangle = cvs.getBoundingClientRect();
// Convert it to real pixels. Ish.
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
// Start drawing…
</script>
细心的读者可能会想知道,如果 dPR 不是整数值,会发生什么情况。问得好,这正是整个问题的关键所在。此外,如果您使用百分比、vh
或其他间接值指定元素的位置或大小,这些值可能会解析为小数 CSS 像素值。具有 margin-left: 33%
的元素最终会显示如下矩形:

getBoundingClientRect()
调用产生的小数像素值。CSS 像素纯粹是虚拟的,因此理论上可以有像素分数,但浏览器如何确定到物理像素的映射?因为不存在小数形式的物理像素。
像素贴合
单位转换过程中负责将元素与物理像素对齐的部分称为“像素贴靠”,顾名思义,它会将小数像素值贴靠到整数物理像素值。具体实现方式因浏览器而异。如果我们在 dPR 为 1 的显示屏上有一个宽度为 791.984px
的元素,一个浏览器可能会以 792px
物理像素呈现该元素,而另一个浏览器可能会以 791px
物理像素呈现该元素。这只是一个像素的偏差,但对于需要像素级精确的渲染来说,一个像素的偏差可能会造成严重影响。这可能会导致模糊,甚至出现更明显的伪影,例如莫尔条纹效应。

(您可能需要在新标签页中打开此图片,才能看到未应用任何缩放效果的图片。)
devicePixelContentBox
devicePixelContentBox
以设备像素(即物理像素)为单位提供元素的内容框。它是 ResizeObserver
的一部分。虽然自 Safari 13.1 以来,所有主要浏览器都支持 ResizeObserver,但 devicePixelContentBox
属性目前仅在 Chrome 84 及更高版本中可用。
如 ResizeObserver
:元素就像 document.onresize
一样,ResizeObserver
的回调函数将在绘制之前和布局之后调用。这意味着,回调的 entries
参数将包含所有被观测元素在绘制之前的尺寸。在上述画布问题的情境下,我们可以利用此机会调整画布上的像素数,确保最终实现画布像素与物理像素之间的一对一精确映射。
const observer = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === canvas);
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
/* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});
observer.observe()
的 options 对象中的 box
属性可让您定义要观测哪些尺寸。因此,虽然每个 ResizeObserverEntry
始终会提供 borderBoxSize
、contentBoxSize
和 devicePixelContentBoxSize
(前提是浏览器支持),但只有在任何受观测的框指标发生变化时,才会调用回调。
借助这一新属性,我们甚至可以为画布的大小和位置设置动画效果(有效保证了像素值的小数部分),并且不会在渲染中看到任何莫尔效应。如果您想了解使用 getBoundingClientRect()
的方法如何产生莫尔效应,以及新的 ResizeObserver
属性如何让您避免这种效应,请在 Chrome 84 或更高版本中查看演示!
功能检测
如需检查用户的浏览器是否支持 devicePixelContentBox
,我们可以观察任何元素,并检查 ResizeObserverEntry
上是否存在相应属性:
function hasDevicePixelContentBox() {
return new Promise((resolve) => {
const ro = new ResizeObserver((entries) => {
resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
ro.disconnect();
});
ro.observe(document.body, {box: ['device-pixel-content-box']});
}).catch(() => false);
}
if (!(await hasDevicePixelContentBox())) {
// The browser does NOT support devicePixelContentBox
}
总结
像素是 Web 上一个非常复杂的主题,但直到现在,您都无法知道元素在用户屏幕上占据的实际像素数。ResizeObserverEntry
中的新 devicePixelContentBox
属性可提供该信息,并允许您使用 <canvas>
进行像素级精确渲染。Chrome 84 及更高版本支持 devicePixelContentBox
。