为什么 Chrome 里小尺寸 JPEG 渲染效果不同
2026 年 8 月 3 日,星期一
为什么 Chrome 里的小尺寸 JPEG 看起来不一样
看似渲染 bug,实则是 Chrome 一个巧妙的 JPEG 解码优化。
这个图标在同事电脑上看起来更好看
不久前在同事电脑上聊天时,我注意到一个 logo 跟我这边的看起来不太一样。在同事那里它显得更细,更贴近原图。它以 15px 渲染,下面是放大后的版本。
注:这不是当时的原图,因为已经过去很久了,我重新做了一张图来演示这个问题。
左边是 Firefox,右边是 Chrome。
凑近看或者往后退一步,Chrome 渲染的会更粗一点。虽然有点奇怪,但把图片换成 SVG 就行了。不过我还是很好奇:为什么它一开始就是这么渲染的?
我深入研究了一下,发现了 Chrome 在小尺寸渲染 JPEG 时用的一种巧妙优化。
缩小图片有时很浪费
最直观的方式是把 JPEG 完全解压到内存里,然后再缩小显示。
但这未必高效。
想象一张 2000 × 2000 的 JPEG,需要显示成 20 × 20。一旦解压,这张图占用的内存远远超过最终需要的体积。原图的位图大约要 12 MB,而最终的 20 × 20 图片只需要约 1.2 KB。缩小之后,大图里的大部分信息都丢失了。
缩小过程中丢失了哪些信息?
一个有趣的发现是:丢失的信息并不是随机的。
图片大幅缩小时,消失的主要是高频细节。这从直觉上很容易理解。想象一棵树,叶子茂密,树皮粗糙:这些细节在像素之间变化很快,属于高频信息。
把这棵树缩小到 20 × 10 这种极小尺寸后,树冠就只剩顶上的一团绿色,树枝就只剩底部的一根棕色线条。缩小后的版本丢掉了那些精细的细节——也就是高频信息。
树的尺寸缩小示意图
由于细节会混合在一起,部分高频信息仍然能在一定程度上保留下来。
JPEG 如何存储图像数据
下面的解释会尽量少用专业术语和数学公式,但会提到一些技术名词,方便你想深入了解时有切入点。同时我会跳过 JPEG 完整变换流程中的不少步骤,因为这里用不到。
JPEG 压缩时,会把图像切成 8 × 8 的小块,再转换到频率域。这个操作叫做DCT(离散余弦变换)。
在 8 × 8 的小块里,最低频率其实是一种纯色。严格来说,它算不上频率,因为画面没有任何变化,所以被称为直流分量。与之相反,最高频率看起来像棋盘格,数值尽可能剧烈地交替变化。介于两者之间的就是频率域的其他内容,这些统称为基函数。
基函数:左上角是纯色,右下角是棋盘格
所以,把 8 × 8 的小块转换到频率域,本质上就是在问:这块图像里每种模式各占多少?这些数量就是系数。
之后 JPEG 还会做几步处理来高效存储这些系数,有损压缩就发生在这一步。但这些细节对本文讨论的内容并不重要。
结合起来看:把 JPEG 缩小到 1/8
现在假设要把一张图片缩小到原来的 1/8。
前面提到的那些 8 × 8 小块,在缩小的图像里就对应一个像素。这个尺寸下,图像基本只需要低频信息,因为就像树的例子那样,高频细节在缩放过程中几乎都消失了。
因此我们不必解压整张 JPEG,可以直接跳过高频部分对应的系数,只用低频系数来生成粗略版本的图像。这样无需先完整展开原图就能得到缩小版的结果。
由于跳过了大量系数,解码后的图像占用空间更小,解压速度也更快。
只要目标缩放比例的分母是 8,就可以按这种方式处理。这种做法的专业名称叫部分 IDCT 缩放*。可以参考 jpegclub.org(读一读你会发现,这项技术其实也可以用来放大图像)。
* 逆离散余弦变换:把频域数据还原为图像数据。
Chrome 是怎么处理的
Chrome 把图像的解码和渲染都交给 Skia 处理。对于 JPEG,Skia 使用的是 libjpeg-turbo,它实现了部分 IDCT 缩放。因此当目标尺寸足够小时,Skia 只会解码低频部分的数据。
换句话说,Chrome/Skia 并不总是先完整解压再缩放。它会算出最接近且分母为 8 的分数比例,按该比例解码,再用更传统的下采样算法继续缩小,直到达到目标尺寸。
这就是为什么我机器上看到的图标显得更粗。因为渲染尺寸非常小,它以八分之一的比例通过部分 IDCT 缩放解码,频域数据里只剩下了直流分量,所有的边缘柔化和渐变信息都被丢弃了。
2026-08-12 订正:有人在 Hacker News 上指出,所使用的缩放算法对最终图像效果也有很大影响。因此实际看到的画质下降,其实是 IDCT 和缩放算法共同作用的结果。
归根结底,结论就是:图标之类的小图不应该用 JPEG。这个格式及其优化都是围绕人们对照片的感知来设计的。
毕竟名字里就写得很清楚:Joint Photographic Experts Group(联合摄影专家组)。