前言
作为一个优秀的前端开发,我对于web项目中引用的图片资源有一种"体积洁癖",不知道你们是不是这样。
但有数据表明这种"洁癖"不是个例,而是大家都默默遵守的隐式规则。
图片体积洁癖
:把图片体积压缩到我能接受的范围(小于1MB),不然心里难受
数据表明
:HTTP Archive: State of Images 平均每个网页请求图片大小总和在"1MB"左右
为了满足这种洁癖,我通常使用 Sharp 来进行压缩图片,借助Claude 3.7
实现了批量一个批量压缩图片的脚本batch-sharp-image。
不出意外的话,我给自己埋下了坑,因为搞不明白Sharp具体的压缩配置项,我使用了其提供的默认配置,喜提Bug:图片压缩过度。这篇文章就来分享一下这次的踩坑经历和一些"图片压缩"的基础知识。
为什么图片可以被压缩?
图片的本质就是1个个像素所组成的玩意,而每个像素都可以使用rgb来描述,是一个 0~255 的整数值,占用一个字节(Byte)的存储空间。
点到为止,想了解更多图片本质的知识推荐此博客:JPEG 图片存储格式与元数据解析 - 知乎
1. 去除"重复"和"不重要"的数据
想象一张纯蓝色的背景图,比如前端开发中常见的按钮或Banner。如果这张图是 1000x1000 像素,传统存储方式需要记录 100 万个像素的 RGB 值。但所有像素都是相同的蓝色,这时候只需要记录:
- 颜色值(例如 RGB(0,0,255))
- 重复次数(1000000次)
这就是行程编码(RLE),存储体积直接从 3MB(100万像素×3字节)降到了几个字节!
这就是典型的无损压缩压缩,每个像素的信息没有被破坏,只是换了种表达方式,原始信息可以还原回去。
2. 人眼"看不到"的数据可以直接丢弃
假设你要压缩一张风景照片,对于其中细腻的天空渐变部分,你可以根据"人眼对亮度敏感,但对颜色细微变化不敏感"的原则,做出以下两种优化:
- 色度抽样 :将颜色信息(CbCr)的采样率降低到亮度(Y)的 1/4(例如 YUV420 格式) 举例:原本每像素保存 Y+Cb+Cr(共3个值),现在每 4 个像素共享一组 CbCr,数据量减少 50%
- 高频细节丢弃:通过 DCT 变换,把图像分解为不同频率的分量,丢掉人眼不敏感的高频细节(如树叶边缘的微小锯齿)
这种属于有损压缩,像 JPEG 就基于此原理。
总结三个核心原因
- 空间冗余 :相邻像素颜色重复(如纯色背景) 前端对应场景:UI 组件的纯色区域、渐变背景
- 视觉冗余 :人眼无法感知的颜色细微差异 前端对应场景:照片中的复杂纹理、阴影过渡
- 编码优化 :用更聪明的数据结构表示颜色(如哈希表调色板) 前端工具 :
sharp
库、ImageMagick
命令行工具
附:压缩效果对比(以 1000x1000 图片为例)
类型 | 原始大小 | PNG 压缩后 | WebP 有损压缩后 |
---|---|---|---|
纯色背景 | 3MB | 10KB | 5KB |
渐变背景 | 3MB | 500KB | 100KB |
复杂照片 | 3MB | 2.5MB | 800KB |
压缩过度是什么情况?
这是个很主观的问题,通常表现有"内容模糊"、"颜色偏差"、"色彩断层"、"有杂色"...
在使用sharp时,我遇到了以下两种问题:色彩断层
& 有杂色
-
默认配置(有杂色)
tsawait sharp(file).png({ quality: 80, dither: 1, // 默认就是1 }).toFile(FilePath);
-
配置二(色彩断层)
tsawait sharp(file).png({ quality: 80, dither: 0, }).toFile(FilePath);
在解决上述两个问题前,让我们先了解一下为什么会有这种情况发生。
色彩断层
色彩断层(色彩带,color banding),多发生在纯净的渐变物体中,比如黄昏天空,棚拍背景,光线投影等等。
断层的问题在小尺寸图片不明显,但在大图上感官就很差。而色彩断层的原因可以用一句话描述:颜色不够用了。
1. 颜色不够用了
图片的"颜色深度"(色深)决定了它能表达多少种颜色。比如:
- 8位色深(如普通JPG):只能显示256种颜色
- 16位色深(如RAW):能显示数万种颜色
举个例子: 假设天空的渐变从深蓝到浅蓝需要500种颜色。但压缩后(比如用8位色深),系统只能用256种颜色来"凑数"。原本平滑的过渡被迫合并成几个大色块,就像楼梯台阶一样,形成断层。
2. 压缩算法"砍掉"了细微变化
有损压缩(如JPEG、WebP)会通过两种方式破坏渐变:
- 色度抽样:把颜色信息的精度降低,比如相邻4个像素共享一组颜色值,导致渐变区域颜色跳跃;
- 量化合并:将相近的颜色强行合并为一种(比如把深蓝A和深蓝B统一成深蓝C),进一步减少颜色种类。
打个比方:
本来平滑的渐变每个像素的颜色值应该是
0,0.25,0.5,0.75,1,1.25,1.5,1.75,2,2.25,2.5...
但是由于"砍掉"了细微变化,导致小数的精度丢失,所以上面一串数字就成了
0,0,0,0,1,1,1,1,2,2,2...
抖色技术
在Sharp的默认png配置中开启了dither: 1
,这是一项名为"抖色/仿色"的技术。 简单来说,通过这项技术可以做到在不增加色深的基础上,让"色彩断层"在视觉感官上不怎么明显。
引用维基百科的描述 Dither - Wikipedia:
抖动技术在计算机图形学中用于在色彩调色板有限的系统中营造出图像具有较高色彩深度的错觉。在抖动图像中,调色板中没有的颜色通过调色板内可用颜色的像素扩散来近似。人眼会将这种扩散视为其中颜色的混合。使用相对较少颜色的调色板生成的抖动图像通常可以通过其特有的颗粒感或斑点状外观来识别。
举个例子:
下面这张猫猫的图片只有黑白两种颜色,在没经过"抖色"处理前,你无法清晰地分辨猫的轮廓。
而抖动技术通过添加"噪点",则可以欺骗人眼看到中间的灰色,让猫猫的轮廓清晰起来,大大提升图片细节。
副作用:有杂色
Sharp 中抖色技术使用到的算法是Floyd--Steinberg dithering - Wikipedia,一种叫做误差扩散颜色抖动(Error Diffusion)的方法,这里不再展开描述,感兴趣可以自行了解: 关于颜色抖动(dithering) - 知乎
虽然"抖色"可以缓解一定程度的"色彩断层",但算法的局限性让他在部分情况下会出现失误,例如我的踩坑案例中,生成过多的"噪点"让图片出现杂色。
Sharp 配置推荐
说了那么多如何硬核的知识点,那么如何解决上述遇到的"压缩过度"问题呢?
调整 Sharp配置时,可以阅读官网文档,来辅助理解每个选项的作用。
当然我不会告诉你,直接询问AI会更加高效更加容易理解😎
Prompt: "给我解释一下这些配置的对于图片压缩的具体作用: ${具体配置}"
解决"压缩过度"
上文有介绍到"颜色不够用"是"色彩断层"问题的本质,而"有杂色"是因为"抖色算法"计算失误了。
ts
await sharp(file).png({
quality: 92, // 从80提升到92,让颜色数量更多
dither: 0.6, // 从1降低到0.6,降低"抖色算法"的失误概率
}).toFile(FilePath);
// 体积变化(原图 2330kb):534kb -> 492kb
// 解决了压缩过度,体积居然还降低了
//(所以说"抖色算法"的副作用还可能带来体积的增加)
我的Sharp预设
sharp 不只可以输出png格式,还支持其他常见的图片格式(webp, jpeg, avif...)
而不同格式的sharp配置不同,输出的图片效果好坏,图片体积大小都有差异。
因此我分享一下我的压缩图片预设给你们:
-
在绝大情况下优先选择webp格式,体积最小的同时观管上无明显差别
除了一些较老的安卓版本webview不支持
-
其次在不需要透明背景的图片,优先选择jpg格式
-
最后需要"无损压缩"的图片,选择png格式
ts
// webp配置
await sharp(file).webp({
quality: 92,
alphaQuality: 90,
// 6种取值自行选择:default, photo, picture, drawing, icon, text
preset: 'text', // 针对含文字内容的图像优化
}).toFile(tempFilePath);
// jpeg配置
await sharp(file).jpeg({
quality: 92,
mozjpeg: true, // 更先进的量化和编码算法,但会增加压缩时长
}).toFile(tempFilePath);
// png配置(无损压缩)
await sharp(file).png({
palette: false
adaptiveFiltering: true,
progressive: true
}).toFile(tempFilePath);
参考文档
- Lossy Image Compression with Dithering
- Why Your Website Should Not Use Dithered Images - Simple Thread
- JPEG 图片存储格式与元数据解析 - 知乎
- 关于颜色抖动(dithering) - 知乎
附录1:Webp 配置详细介绍
分享一下让AI解释的Sharp 每个配置的作用,未校验其正确性,仅供参考
基本质量选项
quality
- 类型 :
number | undefined
- 默认值 :
80
- 范围 :
1-100
- 作用:控制有损压缩的质量。值越高,图像质量越好,但文件大小也越大。
- 使用场景:通常 75-85 是质量和文件大小的良好平衡点。
alphaQuality
- 类型 :
number | undefined
- 默认值 :
100
- 范围 :
0-100
- 作用:控制透明度通道(alpha 通道)的质量。
- 使用场景:如果图像含有透明度但透明区域不需要高精度,可以降低此值减小文件大小。
压缩模式选项
lossless
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:启用完全无损压缩模式,保留原图的所有细节和质量。
- 使用场景:适用于需要保持原图像精确细节的场景,如文字、线条图或需要多次编辑的图像。
- 注意:文件大小通常比有损压缩大很多。
nearLossless
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:使用接近无损的压缩模式,在视觉上几乎无损但有更好的压缩率。
- 使用场景:当你需要高质量图像但仍能接受微小的视觉差异以获得更小文件大小时。
高级压缩选项
smartSubsample
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:使用高质量的色度子采样技术,改善颜色边界处理。
- 使用场景:对于包含清晰色彩边界的图像(如文字、图标)很有用。
effort
- 类型 :
number | undefined
- 默认值 :
4
- 范围 :
0-6
- 作用:控制 CPU 处理强度,值越高压缩效果越好但速度越慢。
- 使用场景 :
0-2
:快速压缩,适合开发环境3-4
:平衡速度和压缩率,适合大多数生产环境5-6
:最大压缩,适合追求极致文件大小的场景
动画相关选项
minSize
- 类型 :
boolean
- 默认值 :
false
- 作用:通过阻止使用关键帧来最小化文件大小(处理较慢)。
- 使用场景:处理 WebP 动画文件并优先考虑文件大小而非编码速度时。
mixed
- 类型 :
boolean
- 默认值 :
false
- 作用:允许在动画帧中混合使用有损和无损压缩(处理较慢)。
- 使用场景:优化动画 WebP,允许系统根据帧内容选择最合适的压缩方法。
预设选项
preset
- 类型 :
keyof PresetEnum | undefined
- 默认值 :
'default'
- 可选值 :
'default'
,'photo'
,'picture'
,'drawing'
,'icon'
,'text'
- 作用:使用预定义的配置集合适合特定类型的图像。
- 使用场景 :
default
:适用于大多数图像photo
:针对照片优化picture
:针对一般图片优化drawing
:针对线条图和绘图优化icon
:针对小图标优化text
:针对含文字内容的图像优化
附录2:Jpeg 配置详细介绍
分享一下让AI解释的Sharp 每个配置的作用,未校验其正确性,仅供参考
基础质量控制
quality
- 类型 :
number | undefined
- 默认值 :
80
- 范围 :
1-100
- 作用:控制图像压缩质量,值越高图像质量越好,但文件大小也越大
- 压缩效果 :
90-100
:接近原图质量,极小的压缩率,文件较大70-89
:优质压缩,肉眼很难察觉质量下降50-69
:明显压缩,可见一些压缩痕迹,但对大多数网络用途足够30-49
:高度压缩,明显质量下降,但文件尺寸显著减小1-29
:极度压缩,质量较差,通常不推荐
编码格式优化
progressive
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:启用渐进式(隔行)扫描
- 压缩效果 :
- 渐进式 JPEG 在网络加载时会逐步显示从模糊到清晰的图像
- 适合大尺寸图像、慢速网络环境
- 对较小图像(< 10KB)文件尺寸可能增大
chromaSubsampling
- 类型 :
string | undefined
- 默认值 :
'4:2:0'
- 可选值 :
'4:4:4'
(无子采样),'4:2:0'
(标准子采样) - 作用:控制色度通道的采样方式
- 压缩效果 :
'4:2:0'
:将色彩信息降采样,大幅减小文件体积,人眼很难察觉质量变化'4:4:4'
:保留所有色彩信息,适用于需要精确色彩的图像(如产品照片)- 当
quality > 90
时建议使用'4:4:4'
以保持高质量
高级优化选项
trellisQuantisation
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:使用格栅量化算法优化图像
- 压缩效果 :
- 能在相同质量下进一步减小文件体积(约 3-8%)
- 增加编码时间,但提高压缩效率
- 与
mozjpeg
一起使用效果更佳
overshootDeringing
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:减少压缩产生的边缘振铃效应
- 压缩效果 :
- 改善高对比度边缘的视觉效果
- 减少锯齿和模糊现象
- 对文本和线条丰富的图片效果明显
optimiseScans
/ optimizeScans
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:优化渐进式 JPEG 的扫描层级
- 压缩效果 :
- 进一步减小文件体积(约 3-10%)
- 会强制启用
progressive
模式 - 显著增加编码时间,但适合批处理
optimiseCoding
/ optimizeCoding
- 类型 :
boolean | undefined
- 默认值 :
true
- 作用:优化 Huffman 编码表
- 压缩效果 :
- 不影响图像质量,但减小文件大小(约 2-5%)
- 编码速度略有影响,但收益显著
quantisationTable
/ quantizationTable
- 类型 :
number | undefined
- 默认值 :
0
- 范围 :
0-8
- 作用:选择量化表
- 压缩效果 :
- 不同的量化表适合不同类型的图像
0
:通用目的,适合大多数图片2-3
:更好的细节保留,适合照片8
:更激进的压缩,适合网络图像
mozjpeg
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:使用 Mozilla 的优化 JPEG 编码器默认设置
- 压缩效果 :
- 使用更先进的量化和编码算法
- 同等质量下可减小 10-15% 的文件大小
- 编码时间显著增加(约 8-10 倍)
- 自动启用多项高级优化,如
trellisQuantisation
和overshootDeringing
附录3:Png 配置详细介绍
分享一下让AI解释的Sharp 每个配置的作用,未校验其正确性,仅供参考
基本格式控制
progressive
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:启用渐进式(隔行)扫描
- 压缩效果 :
- 启用后,图像数据会被重新组织成多次扫描,使浏览器能够渐进式渲染图像
- 在慢速网络下可改善用户体验,但通常会使文件尺寸略微增大约 5-10%
- 主要用于改善加载体验而非压缩效果
压缩控制
compressionLevel
- 类型 :
number | undefined
- 默认值 :
6
- 范围 :
0-9
- 作用:控制 zlib 压缩程度
- 压缩效果 :
0
:不压缩,速度最快但文件最大1-3
:快速压缩,适合开发环境4-6
:平衡压缩率和速度7-9
:最大化压缩率,适合生产环境但处理速度慢- 增加压缩级别时,文件大小通常能减少 5-15%,但压缩时间会明显增加
adaptiveFiltering
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:使用自适应行过滤算法,优化每一行数据的压缩效率
- 压缩效果 :
- 可减少文件大小约 5-20%,特别是对于具有渐变或复杂模式的图像
- 会显著增加编码时间
- 适合对文件大小有严格要求的生产环境
颜色优化选项
quality
- 类型 :
number | undefined
- 默认值 :
100
- 范围 :
1-100
- 作用:决定使用多少颜色来表示图像(在使用调色板时)
- 压缩效果 :
- 降低质量会减少颜色数量,从而减小文件大小
- 只有当
palette: true
时才有效果 - 值越低,压缩率越高,但可能导致明显的色彩带和失真
- 相比原图可减小 20-80% 的文件大小
effort
- 类型 :
number | undefined
- 默认值 :
7
- 范围 :
1-10
- 作用 :控制 CPU 处理强度,自动设置
palette: true
- 压缩效果 :
1-3
:快速处理,适合开发环境4-6
:平衡处理时间和优化效果7-10
:最大优化,但处理速度慢- 会将图像转换为调色板模式,大幅减小文件大小(通常 30-70%)
palette
- 类型 :
boolean | undefined
- 默认值 :
false
- 作用:将图像转换为调色板(索引)模式,支持透明度
- 压缩效果 :
- 使用有限的颜色集来表示图像,大幅减小文件大小
- 对于颜色较少的图像(如图标、UI元素、卡通风格图像)特别有效
- 对于照片和渐变可能会导致明显的色带问题
- 文件大小可减少 30-80%
colours
/ colors
- 类型 :
number | undefined
- 默认值 :
256
- 范围:实际范围是 2-256
- 作用:控制调色板中的最大颜色数量
- 压缩效果 :
- 减少颜色数量可显著减小文件大小
- 值越小,压缩率越高,但图像质量下降越明显
- 常用值:
256
:对于需要高质量的图像32-128
:大多数图标和 UI 元素8-16
:简单图标和单色图像
dither
- 类型 :
number | undefined
- 默认值 :
1.0
- 范围 :
0.0-1.0
- 作用:Floyd-Steinberg 抖动算法强度
- 压缩效果 :
- 通过抖动算法缓解调色板模式下的色带问题
0.0
:关闭抖动,最小文件大小但可能有明显色带0.5
:中等抖动,平衡文件大小和视觉效果1.0
:最大抖动,最佳视觉效果但文件稍大- 启用抖动可使用更少的颜色获得更好的视觉效果