为什么很小的 JPEG 在 Chrome 里看起来不一样

查看原文 HN 讨论

文章摘要

作者(HN 上的 gutechh)在 2026 年 8 月 3 日写下这篇短文,起因是一次很日常的观察:他和同事一起看同事电脑上的界面时,注意到某个 logo 和自己机器上长得不一样——同事那边更细、更接近原图,自己这边更粗。那个 logo 渲染尺寸是 15px。差别就在浏览器:一边是 Firefox,一边是 Chrome;把图换成 SVG 就解决了,但他还是好奇为什么一开始会这样渲染。追下去发现的是 Chrome 在小尺寸渲染 JPEG 时用的一个「巧妙的解码优化」。

为什么缩小图片可能很浪费。 直觉做法是把 JPEG 完整解压到内存里再缩小。但这并不总是高效:想象一张 2000 × 2000 的 JPEG 要显示成 20 × 20,完整位图大约占 12 MB,而最终的 20 × 20 图只需要大约 1.2 KB——大图里绝大部分信息在缩小过程中都被丢掉了。

丢掉的是什么信息。 关键洞察是:丢掉的信息不是随机的。图片被大幅缩小时,消失的主要是高频细节。作者用一棵树举例:满是叶子的树冠和粗糙的树皮,这些细节在像素之间快速变化,属于高频信息;把这棵树缩到 20 × 10 这么小,最后只剩上面一团绿(树冠)和下面一根褐色的棍子(树干),精细的细节、也就是高频信息,都被丢掉了。当然有一部分高频信息还是会以「混在一起」的形式残留。

JPEG 是怎么存图像数据的。 压缩过程中,图像被切成 8 × 8 的块并转换到频域,这个运算叫 DCT(离散余弦变换)。在一个 8 × 8 块里,最低的「频率」是纯色——严格说它不算频率,因为什么都没变,它是常量分量(constant component);另一端最高的频率长得像棋盘格,值变化到最大程度;中间的一切代表频域的其余部分,这些叫基函数(basis functions)。所以把一个 8 × 8 块转换到频域,本质上是在问:这个块里每种图案各有多少?这些「多少」就是系数(coefficients)。JPEG 后面还有几步用来高效存储这些系数,有损压缩发生在那里,但对本文讨论不重要。

以 1/8 比例渲染 JPEG。 假设要把图缩小 8 倍。那些 8 × 8 的块现在正好可以由缩小图里的单个像素来表示。在这个尺寸下,图像主要只需要低频信息,因为就像树的例子一样高频细节基本都会在缩放中消失。于是不必解压整张 JPEG,可以直接跳过高频部分的系数,只用生成粗略版本所需的那些——这样就得到缩小结果而无需先把原图完整展开。解码出的图更小、解压也更快,因为跳过了相当一部分系数。这个技巧可以推广到其他比例,只要是分母为 8 的分数。它的技术名称是 partial IDCT scaling(部分逆离散余弦变换缩放,IDCT 即把频域变回图像域);作者指向 jpegclub.org,并提到这个技巧同样可以用来放大图像。

Chrome 在其中的位置。 Chrome 把图像解码和渲染委托给 Skia,而 Skia 对 JPEG 使用 libjpeg-turbo,后者实现了 partial IDCT scaling,因此当目标尺寸足够小时它可以只解码较低频的数据。换句话说,Chrome/Skia 并不总是先完整解压再缩放:它会算出最接近的、分母为 8 的分数,按那个比例解码,然后再用更传统的降采样算法继续缩到目标尺寸。这就是那个图在他机器上显得更粗的原因——因为渲染得太小,它被以 1/8 比例用 partial IDCT scaling 解码,于是频域表示里只剩下常量分量,所有边缘柔化和渐变都没被用上。

文章在 2026 年 8 月 12 日加了一条更正:有人在 Hacker News 上指出所用的缩放算法本身也起了很大作用,所以最终的画质退化实际上是 IDCT 与缩放算法共同造成的混合结果。

结论一句话:不要用 JPEG 来做图标之类的东西。这个格式及其优化都是围绕我们对照片的感知设计的——毕竟名字里就写着,Joint Photographic Experts Group。

HN 评论精华

这条帖子 339 分、67 条评论。讨论主线有三条:(一)「那 Firefox 是怎么做的?」——文章只讲了一半,评论区补齐了另一半,并且这条追问直接促成了文章末尾的更正;(二)缩放算法本身(模糊 vs 振铃)到底贡献了多少,以及客观画质指标与主观感受的分歧;(三)从「别用 JPEG 做图标」延伸出的一大段 SVG 实战经验,包括一段意外跑题到 XML 命名空间的吐槽。作者 gutechh 全程在场并很诚实地接受修正。