Skip to content
返回博客
教程

图片压缩后反而变大?三个原因与实测数据

压缩完体积不降反升,工具没坏,是浏览器 canvas 把图重编码成了 32 位 RGBA。实测 83 张真实 PNG,83 张全部变大。讲清三种成因与对应解法,附免费在线压缩工具。

14 分钟

图片压缩后反而变大?三个原因与实测数据

你的压缩工具大概率没坏。输出的体积和输入一样,或者明显更大,这种情况的原因几乎总是三种之一,而「工具坏了」不在其中。

先说最常见的那一种。凡是通过浏览器 canvas 元素做压缩的工具,都会丢掉 PNG 原有的色彩类型(color type),把每个像素重新编码成 32 位 RGBA。我们把本站全部 83 张 Open Graph 卡图 PNG 在 Chrome 151.0.0.0 里过了一遍 canvas.toBlob('image/png'),83 张无一例外全部变大。膨胀幅度中位数 +76.6%,最小 +35.6%,最大 +237.2%

第二种成因:你的 JPEG 本来就已经压过了。用质量 0.8 连压五轮,体积从第二轮起就不再动,而每一轮都在继续糟蹋画面。

第三种:你的 PNG 已经被量化(quantize)过了,没有剩余的色彩冗余可供第二次压缩去除。

这三种都不是把质量滑块往下拖能解决的。解法是换一种格式,或者换一个编码器。同样这 83 张 PNG 转成质量 0.8 的 WebP 之后全部变小,中位数 −94.2%

这些数字是怎么测出来的。 Chrome 151.0.0.0(由 Playwright 驱动)、macOS 26.5.2、Node v25.8.2、upng-js 2.1.0、ImageMagick。83 张的样本是本站 public/og 目录下的全部 PNG,不是从中抽样。另有 4 张 1200×630 与 800×600 的受控构造图,分别覆盖照片、图形、调色板与 JPEG 四种情形。

1. 图片压缩不起作用:30 秒分诊

不管你做的是照片压缩还是截图压缩,都先对号入座,然后直接读它指向的那一节。

你输入的是你得到的是根本原因跳到
PNG 截图或图形比原图更大canvas 把它重编码成了 32 位 RGBA第 2 节
JPEG 照片比原图更大它被存成了 PNG第 2 节
JPEG 照片几乎没有变化已经到了体积平台期第 3 节
已经过一次压缩工具的 PNG没变化,或略大一点没有余量了第 4 节
任何图变小了,但发糊或偏色世代损失(generation loss),或 ICC 配置文件被剥离第 3 节和第 6 节

这几行并不互斥。把一张 JPEG 照片丢进基于 canvas 的 PNG 导出器,前两行会同时命中,一个 47,828 字节的文件就是这样变成 809,415 字节的。

如果你不想诊断,只想直接拿到更小的文件,我们的在线图片压缩工具走的是量化 PNG 编码器,而不是 canvas 往返;并且只要压缩结果不比你上传的文件更小,它就会丢弃自己的输出。

2. 成因一:浏览器 canvas 永远写出 32 位 RGBA

canvas.toBlob() 到底对你的 PNG 做了什么

canvas 往返里根本没有「压缩」这一步,只有一次解码和一次重编码(re-encode),中间浏览器会把所有不属于像素值的东西扔掉。

把图片画到 canvas 上,浏览器会把它解码成一段扁平的 RGBA 缓冲区:每像素 4 字节,调色板、位深技巧和元数据一概不留。接着 HTMLCanvasElement.toBlob() 从零开始重新编码这段缓冲区。PNG 规范定义了六种色彩类型,PNG 编码器可以自由挑选能表示这张图的最省的那一种。Chrome 的 canvas 编码器不做挑选,它永远输出 color type 6。

输入的色彩类型canvas.toBlob('image/png') 还给你的
RGB(color type 2)RGBA(color type 6)
调色板(color type 3)RGBA(color type 6)
JPEG(没有 PNG 色彩类型)RGBA(color type 6)

这一点可以直接从字节里读出来。PNG 文件的第 25 字节是位深,第 26 字节是色彩类型,两者都在 IHDR 数据块里:

xxd -s 24 -l 2 -p suspect.png
# 0806  ->  位深 8,color type 6(RGBA)

上面三种输入全部得到 depth=8 type=6。一张 64 色调色板图原本每像素只存 1 字节外加一张小表;往返之后,它每像素存 4 字节,而那张表没了。deflate 能找回一部分,但永远找不回全部。

下面这段控制台代码可以在你自己的浏览器里复现:选一个文件,做一次往返,打印前后的体积和色彩类型。

const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg';
input.onchange = async () => {
  const file = input.files[0];
  const bitmap = await createImageBitmap(file);
  const canvas = document.createElement('canvas');
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d').drawImage(bitmap, 0, 0);
  const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
  const head = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
  console.log(file.name, file.size, '->', blob.size);
  console.log('bit depth', head[24], 'color type', head[25]);
};
input.click();

image/png 来说,toBlob 的 quality 参数会被忽略。PNG 是无损的,没有什么可以拿来交换的余地;基于 canvas 的工具里那个看起来在控制 PNG 压缩率的滑块,其实什么都没控制。

实测 83 张:每一张压缩后都比原图更大

完整结果如下,一张都没有筛掉:

指标数值
测试文件数83public/og 里的全部 PNG,同时含 RGB 与调色板两种色彩类型)
变大的文件数83 / 83(100%)
最小膨胀幅度+35.6%
膨胀幅度中位数+76.6%
最大膨胀幅度+237.2%

举一张具体的,好让你看清它的形状:aes-decrypt.png 从 505,516 B 变成 898,014 B,膨胀 +77.6%。同一张图按质量 0.8 编码成 WebP 只有 27,188 B,−94.6%

样本口径这里要说清楚。我们测的这 83 张是 Open Graph 卡图:1200×630、纯色背景、大字号文字、少数几种品牌色。它们恰恰是优秀 PNG 编码器最擅长处理的那类图形,也正因如此,canvas 往返对它们的伤害才格外大。这个结论对图形型 PNG 很有力,但它不等于「世界上任何一张 PNG 经过 canvas 往返都会变大」。一张本来就以完整 RGBA 存储的照片型 PNG,可损失的余地要小得多。

能确立的是:如果你的压缩结果比原图更大,而这个工具跑在浏览器标签页里,那么第一个该查的是编码器,不是你的设置。

为什么一张存成 PNG 的 JPEG 会涨 16.9 倍

这是整份数据里最触目的一行。四张受控文件,全部走同一条 canvas 路径:

文件特征原始canvas PNG变化JPEG q92JPEG q80WebP q80
og-a.png图形,2,351 色,RGB306,302607,481+98.3%56,45937,69513,852
quantized.png64 色调色板59,843184,856+208.9%78,38844,44216,046
photo.png照片,479,373 色498,639867,763+74.0%77,32145,05024,068
photo.jpgJPEG q8247,828809,415+1,592%(16.9×)58,762 (+22.9%)47,15223,862

最后一行是:进去 47,828 字节,出来 809,415 字节。

这个机制解释了一整类「压缩反而变大」的报告。JPEG 是有损的频域编解码器:它把 8×8 的像素块变换成 DCT 系数,做激进的量化,然后只保存幸存下来的那些。PNG 是无损的空域编解码器:它用邻近像素预测当前像素,再对残差做 deflate。解码一张 JPEG,你拿到的像素里带着量化器引入的全部伪影:边缘附近的振铃、平滑渐变里的块状色阶、原图本来没有的细微噪点。

把这些像素存成 PNG,就等于要求一个无损编解码器把这些伪影一个不落地精确保存下来。它照做了。当初 JPEG 为了把文件变小而制造出来的噪声,现在成了让 PNG 变大的元凶,因为噪声恰恰是预测型无损编码器最压不动的东西。

凡是照片前面挡着一个「存为 PNG」默认项的地方,这件事就在发生:截图工具、设计稿导出、聊天客户端、某些上传组件。

同一批像素,差 3.46 倍,两边都是无损

canvas 编码器的弱是可以量化的。取 og-a.png 的像素缓冲区,用两种方式编码,两种都完全无损:

编码器输出色彩类型
Chrome canvas toBlob('image/png')607,481 BRGBA(强制升位)
upng-js encode(..., cnum=0)175,491 BRGB(保留原色彩类型)

3.46×,像素完全相同。「无损」这一点我们验证过:把 upng-js 在 cnum=0 下的输出解码回来,得到的缓冲区与输入的 RGBA 缓冲区逐字节相同。这 3.46× 没有拿任何东西去换。

这也解释了一个常被问起的现象:为什么两个在线压缩工具,都免费,宣传的是同一件事,结果却完全没有可比性?因为「在浏览器里压缩」这句话可以指代两种截然不同的实现。一种把像素交给 canvas.toBlob,然后原样交付它返回的东西。另一种自带 PNG 编码器,并且控制色彩类型。同样的输入,同样的浏览器,差 3.46×。

3. 成因二:你的 JPEG 已经无油可榨

连压五轮,体积不再动

photo.jpg 本来就是以质量 82 保存的。用质量 0.8 对它连续重压五次,每一代都喂给下一代:

世代字节相对上一代
0(原图,q82)47,828
147,152−1.4%
247,164+0.0%
347,156−0.0%
447,1560.0%
547,1560.0%

第一轮给你换来 1.4%。从第 2 代开始,体积锁死在 ±12 字节的区间内,第 4 代和第 5 代的字节数与第 3 代完全一致。

当输入是 JPEG 时,「压缩不起作用」就是这个样子。工具跑了,编码器也跑了,只是没有什么可以再拿掉。质量 80 的量化表要清零的那些系数,质量 82 基本上已经先清掉了,而一个系数一旦没了,就没法再拿掉第二次。

你可以在本地看着它发生:

cp photo.jpg gen0.jpg
for i in 1 2 3 4 5; do
  magick "gen$((i-1)).jpg" -quality 80 "gen$i.jpg"
done
stat -f%z gen0.jpg gen1.jpg gen2.jpg gen3.jpg gen4.jpg gen5.jpg

Linux 上把它换成 stat -c%s。具体字节数取决于你的 ImageMagick 构建链接的是哪个编码器,所以别指望上面那张表能一位不差地复现出来。要看的是形状:一次有意义的下降,然后是一条平线。

把质量数值调高不叫压缩

回头看第 2 节里 photo.jpg 那一行。把这张质量 82 的原图按质量 92 重压,得到 58,762 字节:+22.9%

把质量参数当成一个从「小」到「大」随便拧的旋钮,就会在这里翻车。质量参数选的是一张量化表,而不是一个绝对的画质目标;把一张解码出来的图再送进一张比当初生成它时更细的表,只会把已有的伪影存得更精确,同时再叠上一轮新的损失。文件更大,画面更差,两件事一起发生。

规则很简单:永远不要用高于原图保存时的质量设置去重压一张 JPEG。如果你不知道原来是多少,那就干脆别重压,回去找源文件。

世代损失:你看不见的那部分 JPEG 重压缩画质损失

平台期那张表里藏着一个陷阱。第 2 代之后体积不再变化,但图像一直在变。每一轮都要解码成像素、重新变换、重新量化。上一代里刚好卡在阈值边上活下来的系数,下一代就被推过了界。

损坏不会出现在你盯着看的地方。缩略图尺寸下,第 5 代和第 1 代分不出差别。放大到 100%,去看 JPEG 一向最先崩的几个地方:纯色背景上的硬边缘、文字,以及平滑渐变,渐变里的块状伪影会显现为肉眼可见的 8×8 方格。在一条每次部署都重压一遍的构建流水线里,这种损耗会悄无声息地累积好几个月。

我们测的是文件体积,不是感知质量,所以这五代的 PSNR 和 SSIM 我们没有数据,也就不报。仅凭体积数据能证明的已经够用:从第 2 代起,代价全在画质这一侧,收益是零。

偏色是另一种失败,触发条件相同。canvas 不携带元数据,所以一次 toBlob 往返会连同 EXIF 数据块一起把 ICC 配置文件丢掉。一张标着 Display P3 或 Adobe RGB 的图进去,出来就没有标记了,而查看器会按 sRGB 来解释它。像素值没有变,变的是解释它们的那份说明。

4. 成因三:PNG 体积压不下去,是因为它已经被量化过了

如果你的 PNG 已经过了一次压缩工具,第二轮就没有可下手的地方。这就是 quantized.png 那一行:一个 59,843 字节、已经被削减到 64 色调色板的文件,分别走三条路径:

路径结果相对原图
无损重编码(upng cnum=061,377+2.6%
量化到 256 色61,366+2.5%
量化到 64 色61,366+2.5%

每条路径的结果都比原图更大。大得不多,但确实更大,其中还包括把一个本来就只有 64 色的文件再量化到 64 色。

PNG 压缩靠的是去除冗余:重复的颜色、可预测的邻居、一张小调色板。上一轮已经把这些全收走了。剩下的东西接近不可压缩,那点微小的增长来自编码器自身的开销,比如调色板排序略有不同、逐扫描行的滤波器选得不一样、deflate 运气稍差一点。

所以对一张已经优化过的 PNG 显示「节省 0%」是正确结果,不是失败。一个报告了小幅增长、然后保留你原文件的工具,行为是对的;一个照样把更大的文件塞给你的工具,就不对。

5. PNG 的杠杆在量化,不在重编码

无损重编码 vs 256 色 vs 64 色

三个文件各走三种策略,实测结果:

文件原始无损(cnum=0256 色64 色
og-a.png306,302175,491(−42.7%)110,772(−63.8%76,230(−75.1%
photo.png498,639587,863(+17.9%)109,284(−78.1%61,397(−87.7%
quantized.png59,84361,377(+2.6%)61,366(+2.5%)61,366(+2.5%)

无损重编码是这里面最弱的手段。它在图形上赢了 42.7%,在照片上倒赔 17.9%,在已量化的文件上亏了 2.6%。照片型内容能把一个称职的无损 PNG 编码器也顶回去,因为它找不到调色板,相邻像素之间也很难互相预测。

削减主要发生在量化这一侧,差距还不小:同一张图形在 256 色下是 63.8% 对 42.7%,到 64 色是 75.1%。命令行上:

magick logo.png -colors 64 PNG8:logo-64.png
magick logo.png -colors 256 PNG8:logo-256.png

PNG8: 前缀强制输出调色板 PNG。不加它,ImageMagick 可能会减少颜色数,然后仍然写出一个真彩色文件,那样大部分收益就都白费了。

什么时候调色板是安全的,什么时候会出现色带

量化是有损的。它把每个像素映射到一张有限调色板里最接近的那一项,所以问题在于你的内容是否有足够多的不同颜色,让这种映射变得可见。

安全:图标、logo、UI 截图、扁平插画、图表,以及任何有大片均匀色块和硬边缘的内容。这类图通常最多只含几百种不同颜色,所以 256 项的调色板几乎是白送的,甚至 64 色也常常撑得住。

有风险:照片、平滑渐变、柔和投影,以及半透明叠加层。把一段渐变削减到 64 级会产生可见的色带,而抖动(dithering)用噪点换掉色带,那些噪点又会把一部分体积收益吃回去。渐变之上的半透明是所有情形里最难的一种。

透明度要单独检查一遍,因为它能保留多少取决于编码器,而不是量化本身。ImageMagick 的 PNG8: 写的是二值透明:一个像素要么完全不透明,要么完全透明,柔和的抗锯齿边缘回来就变硬了。专用的 PNG 量化器会保留完整的 alpha 通道,柔边也随之保留。如果你的素材带投影或羽化边缘,先对比这两种结果再决定。

结果要在 100% 下检查,不要看缩略图。色带正是那种缩小预览一定会替你藏起来的伪影。

6. 修复:换格式,别反复重压

一张格式决策表

内容用什么为什么
照片WebP,或为了最大兼容性用 JPEG有损频域编码正是照片需要的
截图、UI 图形量化 PNG,或 WebP纯色块、硬边缘、小调色板
图标和 logo有矢量源时用 SVG,否则用量化 PNG矢量没有分辨率问题
任何需要透明的素材WebP 或 PNG两者都带完整 alpha 通道
动画WebP一种格式就够,不必再用 GIF
像素级精确的存档PNG,无损唯一真正需要无损的场景

这是刻意精简的版本。现代各种格式之间的编码效率与浏览器支持情况,另有一篇专文:WebP vs AVIF vs JPEG:2026 图片格式选型实战

同样这 83 张文件,WebP 的结果

那批经 canvas PNG 100% 变大的 83 张文件,改为按质量 0.8 转 WebP:83 张里 83 张变小,中位数 −94.2%。前面提到的那一张 aes-decrypt.png,从 505,516 B 变成 27,188 B,−94.6%

受控文件的结论一致。og-a.png:PNG 是 306,302,WebP q80 是 13,852。photo.png:PNG 是 498,639,WebP q80 是 24,068。photo.jpg:JPEG 是 47,828,WebP q80 是 23,862。

最后这组对比有个前提要说明。质量 0.8 的 WebP 是有损的,所以它和 PNG 不在同一条起跑线上;而且把一张已有的 JPEG 重编码成 WebP,仍然要付出一代画质。该比的是它和你原本准备发布的那个文件,而不是一个假想中的完美原图。

什么时候仍然应该保留 PNG

有些场景下无损是硬需求。这些素材要保留 PNG:会回到设计流程里被再次编辑的素材、用于像素级比对测试的截图、单个颜色位移就会破坏视觉 diff 的 UI 切图,以及后续还要合成、量化伪影会叠加放大的任何素材。这些情况下,如果内容允许就上一个正经的量化编码器,不允许就接受这个体积。

还有一种「变大」跟压缩毫无关系

如果你把图片以 data URI 的形式内联,它在 CSS 或 HTML 里的体积不等于它在磁盘上的体积。Base64 把每 3 字节编码成 4 个字符,另加 padding,所以文本在算术上就比它携带的字节大 +33%,这还没算传输压缩。一张优化到极致的图片,只要一内联就立刻大三分之一。这个取舍什么时候值得做,见我们那篇图片转 Base64 与 Data URI:何时内联图片

7. 动手:浏览器、命令行、构建流水线

在浏览器里

挑选浏览器端工具时,要紧的区别是 PNG 会不会走 canvas。在我们的在线图片压缩工具里,它不走。PNG 输入会被量化到一张调色板,再当成一张正常的 PNG 写回去,alpha 通道完整保留,所以透明和柔边都能存活;质量滑块对应的是调色板大小,而 PNG 本来就会忽略 toBlob 的那个质量参数,这里也用不上它。质量 100 对应无损重编码。JPEG 和 WebP 确实走 canvas,那里的质量参数是真实的,会按你预期的方式起作用。

有一个行为看起来像 bug,其实是设计如此:如果压缩结果不比你上传的文件更小,工具会丢掉自己的输出,保留你原来的字节。对一张已经优化过的 PNG,你会看到「节省 0%」。那正是第 4 节讲的那件事。

处理全部在你自己的机器上完成,工具不会上传任何文件。

在命令行上

cwebp 随 libwebp 一起分发,是验证「换格式能不能解决你的问题」最快的方式:

# 有损 WebP,质量 0-100
cwebp -q 80 photo.png -o photo.webp

# 无损 WebP,压缩强度 0-9
cwebp -z 9 logo.png -o logo.webp

ImageMagick 7 覆盖转换和量化这两类需求:

# PNG 转 JPEG,指定质量
magick photo.png -quality 80 photo.jpg

# 量化为 64 色调色板 PNG
magick logo.png -colors 64 PNG8:logo-64.png

# 剥掉 EXIF 及其他元数据
magick photo.jpg -strip photo-clean.jpg

macOS 上 sips 已经预装,不需要任何依赖:

# 转成 JPEG;formatOptions 接受 0-100 或 low/normal/high/best
sips -s format jpeg -s formatOptions 80 photo.png --out photo.jpg

# 缩放到最长边 1200 px,保持宽高比
sips -Z 1200 photo.jpg --out photo-1200.jpg

# 读回尺寸
sips -g pixelWidth -g pixelHeight photo-1200.jpg

先缩放,再压缩。编码器处理的是像素,而最便宜的像素是根本不存在的那个。

在构建流水线里

一旦这件事从手工变成自动化,决策点就转移到「工作在哪里发生、由哪个库来做」,取舍也随之完全改变:前端图片压缩方案全面解析:浏览器与 Node 工具对比讲的就是这组比较。本文里能带过去的只有一条规则:每次构建都从原始源文件开始压缩,绝不从上一次构建的产物开始。否则流水线会一路沿着世代损失曲线往下走,而文件体积看起来还稳如泰山。

8. 五个不被实测数据支持的说法

「压两次会更小。」 第 1 代换来 1.4%。第 2 到第 5 代始终在 ±12 字节之内,而画面一直在退化。第二轮是纯粹的成本。

「PNG 是无损的,所以它是更好的格式。」 无损是一种属性,不是一种美德。我们的测试照片存成 PNG 是 498,639 字节,存成 WebP q80 是 24,068 字节。一张只会在屏幕上被看到的照片,把它逐比特精确地保存下来,什么都换不到,却要付出绝大部分体积。

「质量拉到 100 最保险。」 把一张质量 82 的 JPEG 按质量 92 重压,结果是 +22.9% 和一张更差的图。一旦超过原图的质量设置,这个数字就不再表示「更保险」,而是开始表示「更大」。

「文件大是因为分辨率高。」 分辨率确实有关系,但在同一分辨率下格式的影响更大。og-a.png 两边都是 1200×630:canvas PNG 是 607,481 字节,WebP q80 是 13,852 字节。像素数完全相同。

「在线压缩工具都一样。」 同样的像素,同样的浏览器,两边都是无损:canvas 输出 607,481 字节,upng-js 输出 175,491 字节。两个把自己描述得一模一样的工具,差了 3.46×。

9. 常见问题

为什么压缩图片之后反而比原图还大?

因为工具是在重编码它,不是在压缩它。浏览器 canvas 的输出永远是 32 位 RGBA 的 PNG,调色板被丢弃,每像素存 4 字节。我们测的 83 张真实 PNG 里,83 张全部变大,膨胀幅度中位数 +76.6%。改成转 WebP 或 JPEG。

为什么我的 PNG 压了也不见变小?

PNG 是无损的,质量滑块没有可交换的东西。真正的削减来自减少颜色数,而如果文件已经被量化过,就没有可减的了。我们那张 64 色的测试 PNG 再量化到 64 色之后,回来是 +2.5%。

把 JPEG 压两次会损失画质吗?

会,而且几乎换不到任何东西。质量 0.8 连压五轮:第一轮从 47,828 字节降到 47,152,接下来四轮锁死在 12 字节以内。体积不再动,而每一轮都在继续重新量化画面。留好你的原图。

想让文件更小,该用 PNG 还是 JPEG?

照片一律用 JPEG 或 WebP,没有例外。我们的测试照片存成 PNG 是 498,639 字节,JPEG q80 是 45,050 字节,WebP q80 是 24,068 字节。PNG 留给扁平图形、锐利文字和需要透明的场景,那里的压缩工作由小调色板来完成。

为什么图片压缩之后变糊了?

两种不同的成因。有损编码器在低质量设置下会在边缘和文字周围产生可见的块状伪影。反复压缩会叠加世代损失,即便体积已经不再变化。如果不是发糊而是偏色,那就是 canvas 往返丢掉了你的 ICC 配置文件,canvas 完全不携带元数据。

能不能在不损失画质的前提下压缩图片?

能,但收益要低得多。无损重编码只是把完全相同的像素写得更高效:我们那张图形从 306,302 字节降到 175,491 字节,解码后经逐字节验证完全一致。同样的做法用在照片上则走了反方向,+17.9%。想在没有可见损失的前提下拿到真实的节省,用质量 80 的 WebP。

只是一张截图,为什么 PNG 这么大?

屏幕截图以完整 RGBA 的 PNG 保存,压缩前每像素 4 字节,而 Retina 屏还会让两个方向的像素数各翻一倍。扁平内容对调色板削减的反应很好:我们那张 Open Graph 卡图在 256 色下减少了 63.8%,在 64 色下减少了 75.1%。

缩放比压缩更能减小文件体积吗?

通常是的,而且两者会叠加。把两个方向都减半,在编码器开始工作之前就去掉了四分之三的像素,而文件体积大体上跟着像素数走。先缩放到你实际展示的尺寸,再压缩一次。把一张全分辨率的相机原图丢进一个缩略图位置,两道工序都浪费了。

标签: image-compression png jpeg webp canvas debugging