
上周帮一个做电商的朋友排查图片加载慢的问题最后追到的根因特别典型他们的商品图在流水线上被反复转存了四次 JPEG从设计稿导出、到 CDN 前的压缩、到运营手动改尺寸、再到二次上传每转一次就掉一次画质最后详情页那张主图放大后衣服纹理全是方格状的马赛克。这件事让我觉得有必要把 JPEG 这个“人人都用、但很少人真的了解”的格式好好梳理一遍。数字图像处理里JPEG 是最容易被当成黑盒的环节——大家只知道 quality 调 85出了问题就往上加到 95结果体积翻了三倍画质也没好多少。这篇内容想解决的问题很具体搞清楚 JPEG 到底在哪些环节丢掉了信息、quality 参数背后真正的数学含义是什么、不同编程库的 quality 语义为什么不能混用、以及在什么场景下你应该坚决放弃 JPEG 换别的格式。适合刚接触图像处理的同学当作避坑手册也适合已经写过几年代码、但对压缩管线还停留在“调个参数能跑就行”的开发同学补一补底层认知。我尽量不堆教科书公式把每个环节为什么这么设计、以及实际工程中会踩到什么坑讲清楚。1. JPEG 的三重身份与它的取舍逻辑1.1 一个格式三个层面容器、算法、编码家族很多人把 JPEG 当成一个东西其实它至少包含三层含义混着理解就会在各种文档里迷路。第一层是文件容器规范定义了字节流怎么组织开头放什么标记、哪里存量化表、图像数据从哪个位置开始这部分由 JFIF 或 EXIF 规范补齐因为 JPEG 标准本身只规定了码流没规定文件怎么存。第二层是有损压缩算法也就是基于离散余弦变换的那套流程包含颜色空间转换、下采样、分块变换、量化和熵编码五个阶段。第三层是编码家族涵盖 baseline顺序编码、progressive渐进编码、lossless无损预测编码、hierarchical分层编码等多个 profile我们日常说的“JPG”九成以上指的是 baseline 顺序编码。这三层的边界在实际开发中会反复撞上。比如你写代码时用到的quality参数它不属于 JPEG 标准定义的任何东西——标准只定义了量化表而 quality 是各个实现库自己发明的一个“量化表缩放系数”的映射函数。所以同一张图用 Python 的 Pillow 存 quality85、用 OpenCV 存 quality85、用命令行工具存 quality85出来的文件大小可能差 20% 以上因为它们的映射公式和默认采样因子都不一样。这个坑我后面会单独拆开讲先记住一句话quality 不是格式规范的一部分是库的实现细节。再比如 progressive 编码它和 baseline 用的是同一套 DCT 和量化流程区别只在于系数的传输顺序——baseline 是按块从上到下扫一遍progressive 是先传所有块的低频分量再分几轮传高频。所以“把一张图另存为渐进式”理论上不额外损失画质只要你不再重新量化。理解了这一点你就知道为什么用专门的工具做无损转换是可行的而用图像编辑软件打开再保存是灾难。1.2 为什么它至今还是默认选项以及默认选项的代价从工程角度看JPEG 能活到今天靠的不是画质是生态惯性和解码成本。任何一个浏览器、任何一台手机、任何一颗图像处理芯片都内置了 JPEG 硬解码通路解码一张 4000×3000 的 JPEG 在现代设备上耗电几乎可以忽略。相比之下 AVIF、HEIF 这些新格式压缩率确实高出一大截但软解开销大、老设备兼容性差、端到端链路拍摄、编辑、CDN、客户端还没完全打通。所以在“兼容性优先、画质够用就行”的场景下JPEG 仍然是理性的默认选择。但默认选择意味着大量的人不做选择。我见过太多项目在图片处理链路上根本没做过任何决策截图直接存 JPEG图标直接存 JPEG带透明通道的设计稿先转 JPEG 再转回来甚至有人把已经压过的 JPEG 解码成位图做锐化再存回 JPEG。每一次这样的往返损失都是叠加的而且不可逆。JPEG 有损压缩的本质决定了它适合做最终交付格式不适合做中间工作格式。工作流里凡是需要反复编辑的素材都应该用 PNG、TIFF 或者干脆用无损压缩的中间格式只在最后输出给用户的那一步才转成 JPEG。还有一个容易被忽略的代价是语义丢失。PNG 里的 alpha 通道、色彩配置文件、图层信息转成 JPEG 之后要么被丢弃要么被压成近似值。等你后面想「把这张图重新做成带透明的按钮」原始信息已经回不来了。我在实际项目里养成的习惯是所有原始素材按目录结构保留无损版本JPEG 只作为派生产物存在任何时候都能从源头重新生成。2. 拆开压缩引擎DCT、量化表与熵编码2.1 第一步就砍一半颜色空间转换与色度下采样JPEG 压缩的第一刀不是砍在空间分辨率上而是砍在颜色上。编码前图像会从 RGB 转换到 YCbCr 空间其中 Y 是亮度Cb 和 Cr 是两个色度分量。这一步本身不损失信息它是可逆的线性变换。真正的损失发生在紧随其后的色度下采样。人眼视觉系统对亮度细节的分辨能力远强于对颜色细节的分辨能力这是生理层面的客观事实。所以 JPEG 允许把 Cb 和 Cr 两个通道的分辨率降低常见模式有三种采样标识常见写法Y 分辨率Cb/Cr 分辨率相对原始数据量典型用途4:4:41x1,1x1,1x1全分辨率全分辨率100%图像编辑中间产物、文字截图、专业输出4:2:22x1,1x1,1x1全分辨率水平减半约 67%视频帧、需要保留水平色边的内容4:2:02x2,1x1,1x1全分辨率水平垂直都减半约 50%绝大多数网络图片、手机拍照默认4:2:0 直接让色度数据量降到四分之一整体样本数从 3 份变成 1.5 份等于在进入 DCT 之前就把数据量砍掉一半。这就是为什么很多人发现 JPEG 里红色细线会糊成一片、小字边缘会出现彩色噪点——红色在 YCbCr 里主要由 Cr 承载而 Cr 通道的分辨率只有原来的四分之一细节早就没了。提示如果你的图像里有大量高饱和度的红色、品红色元素比如价格标签、促销角标4:2:0 会明显让这些区域变糊。这种情况改用 4:4:4体积大概涨 30% 到 50%但色边问题会消失。下采样具体怎么算最常见的做法是盒式平均把相邻的 2×2 个色度像素取平均得到 1 个。也有一些编码器用更平滑的滤波核效果略有差异。解码时的上采样则通常是双线性或最近邻插值不同解码器的实现不完全一致所以同一张 4:2:0 的图在不同软件里看色边表现可能微有差别。2.2 8×8 分块与 DCT把空间信息搬到频域下采样之后每个通道都被切成独立的 8×8 像素块块与块之间不重叠。这个 8×8 的尺寸是当年算力和压缩效率折中的产物——块太小低频表达不足、块边界太多块太大计算复杂度飙升且块内内容差异过大反而影响压缩。四十多年过去这个数字一直没变。每个 8×8 块做二维离散余弦变换把 64 个空间像素值映射成 64 个频率系数。变换本身是可逆且无损的它只是换了一种表达方式。系数矩阵左上角是直流分量DC代表这个块的平均亮度越往右下角走频率越高代表块内越来越精细的纹理变化。自然图像的能量高度集中绝大多数块的系数能量都堆在左上角那几个位置右下角的高频系数通常非常接近零。这里有个很重要的直觉一张图里变化平缓的区域比如天空、墙面、皮肤变换后高频系数几乎全是零而变化剧烈的区域比如树叶、毛发、织物纹理高频系数才会有明显数值。这也解释了为什么人像照片比风景照片更容易压得小以及为什么压缩后的图像在纹理丰富处最容易出现糊感。二维 DCT 的公式长这样本质就是两组一维余弦基函数的乘积求和F(u,v) (1/4) * C(u) * C(v) * ΣΣ f(x,y) * cos((2x1)uπ/16) * cos((2y1)vπ/16) 其中 C(0) 1/√2C(k) 1 (k0) f(x,y) 是 8×8 块内的像素值已经减去 128 做零均值化 F(u,v) 是对应的频率系数零均值化这一步经常被忽略——编码前每个像素值要减去 128让取值范围从 0~255 变成 -128~127。原因是 DCT 的 DC 系数等于块内所有像素的和如果不做零均值化DC 系数会非常大占用更多比特。这个细节在你自己实现编码器时会直接影响码率。2.3 量化真正造成不可逆损失的那一刀前面所有步骤都是可逆的只有量化是不可逆的。量化就是把 DCT 系数除以量化表里对应位置的数值然后四舍五入取整Fq(u,v) round(F(u,v) / Q(u,v))Q 就是量化表一张 8×8 的整数矩阵。Q 值越大除完之后的系数越容易变成 0压缩率越高画质越差。标准里给了两张推荐的基准表一张针对亮度、一张针对色度色度表整体数值更大因为人眼对颜色误差更不敏感。观察标准亮度量化表你会发现一个规律左上角数值很小DC 位置是 16右下角数值很大最大到 99。这意味着低频系数被保留得比较精细高频系数被大刀阔斧地舍弃。这正是利用人眼特性的经典设计——高频细节人眼本来就分辨不清扔了不心疼。量化之后典型块的 64 个系数里可能有 50 个以上变成 0。这为后面的熵编码创造了极好的条件因为连续的零可以用很短的游程编码表示。注意量化表不是只能固定的。很多编码器支持自定义量化表比如给文字类图像用更平缓的高频衰减曲线能保住笔画边缘给漫画用另一套表避免大面积纯色区域产生色带。但有代价——自定义表必须写进文件里解码器要能读某些老设备对非标准表的支持不完整会出兼容问题。2.4 熵编码与文件骨架从系数到字节流量化后的系数要经过两步处理才能变成比特流。第一步是之字形扫描把 8×8 矩阵按从左上到右下的斜线顺序读成一维序列这样能让低频系数排在前面、高频零值集中在尾部形成长长的零尾巴。第二步是熵编码DC 系数用与前一个块 DC 值的差分来编码因为相邻块的平均亮度通常接近AC 系数用游程加幅值的组合编码最后套一层霍夫曼表做变长编码。霍夫曼表也有讲究。标准里给了两张推荐的亮度表和两张色度表编码器可以原样用也可以根据实际图像的统计特性自己生成最优表写进文件。自己生成通常能再省 5% 到 10%代价是解码端要多读一张表。这也是为什么同样 quality 参数下开了optimize选项的文件会小一圈。文件结构层面一个标准 JPEG 的字节流大致是这样组织的标记十六进制作用SOIFFD8文件开始APP0FFE0JFIF 头存版本、密度单位、缩略图信息APP1FFE1EXIF 元数据存拍摄参数、方向、GPS 等DQTFFDB量化表可能有多张亮度 色度SOF0FFC0baseline 帧头存宽高、通道数、采样因子SOF2FFC2progressive 帧头出现在渐进式文件里DHTFFC4霍夫曼表可能有四张DRIFFDD重启间隔用于错误恢复SOSFFDA扫描开始后面紧跟熵编码数据EOIFFD9文件结束用十六进制工具打开任意一张 JPEG头两个字节必然是FF D8末尾两个字节必然是FF D9。有些场景下文件损坏了但头尾还在就会出现“能识别成图片但显示一半”的现象排查时可以先用这个特征快速判断是不是截断问题。3. 质量参数是个“假旋钮”q 值、体积与画质的真实关系3.1 三种 quality 语义以及跨库迁移时的坑前面提过 quality 不是标准的一部分问题在于不同库对它的实现差异比想象中大。目前主流的有三种映射方式第一种是 libjpeg 系的缩放公式这是流传最广的实现。它把 quality 从 1~100 映射到量化表的缩放因子 Sif q 50: S 5000 / q else: S 200 - 2 * q代入几个值算一下就有感觉了q50 时 S100也就是原样使用标准量化表q75 时 S50所有量化表数值减半画质明显提升q90 时 S20量化表数值只有标准的五分之一q100 时 S0理论上所有量化步长为 1此时量化不再是损失来源但 DCT 系数本身仍是浮点变整数所以还是有一点点舍入误差。第二种是 Pillow 的 quality 参数它底层走的就是上面这套公式但还额外挂了subsampling参数。Pillow 的subsampling默认值是 -1实际行为是根据 quality 自动选择quality 高于 95 用 4:4:4低于等于 95 用 4:2:0。这个隐式切换是很多人踩坑的地方——你以为只改了 quality实际上采样因子也变了文件大小跳变的幅度超出预期。第三种是某些商业软件自定义的曲线比如一些图像编辑软件用的是感知质量模型映射关系和 libjpeg 完全不同。同样标称 quality80不同软件出来的文件大小能差一倍。提示做跨库迁移时绝对不要假设“Pillow 存 85 和 OpenCV 存 85 是一回事”。正确做法是固定量化表或者在切换库之后重新做一轮体积和画质的实测对齐。我在一个项目里就因为从 Pillow 换到另一套编码库导致所有图片体积突然涨了 40%CDN 流量账单直接多出一截。3.2 实测数据同图不同 q 值的体积与观感我拿一张 4000×3000 的风光照片做过一轮完整测试统一用 4:2:0、开启 optimize、关闭 progressive只变 quality结果如下quality缩放因子 S文件体积相对 q95 的体积主观观感10008.9 MB320%和原图肉眼难分95103.6 MB100%基准几乎无可见损失90202.4 MB67%放大到 100% 才能看出轻微差异85301.9 MB53%正常观看距离下无法分辨80401.5 MB42%天空等平滑区域开始出现轻微色带75501.2 MB33%云层细节变平细草地质感下降60830.8 MB22%明显块效应文字边缘发虚401250.5 MB14%只能看个大概轮廓从数据能看出几个关键拐点。q 从 95 降到 85体积省了接近一半画质损失几乎不可察觉这是性价比最高的一段。q 从 85 降到 75体积再省 37%但已经开始出现可辨认的画质劣化尤其是平滑渐变区域。q 低于 60 之后曲线开始变缓省下的体积不多但画质断崖式下跌属于明显的亏本买卖。工程上的经验阈值网页正文配图用 80~85商品主图用 85~90摄影作品展示用 90~95任何场景下超过 95 都基本没有收益纯属浪费带宽。低于 70 就要非常谨慎除非你的图本来就是缩略图规模。3.3 采样因子的组合与选择采样因子和 quality 是两个独立的维度组合起来才是完整的配置。很多人只调 quality 不调采样因子结果发现怎么调都调不出想要的效果。选 4:4:4 的典型场景包含文字的截图或图表、有大量红色细节、需要后续再做抠图或合成、专业印刷输出。文字图尤其要注意4:2:0 会让黑色文字的边缘染上彩色噪点看起来像没对焦。选 4:2:2 的场景比较少主要是视频编码的遗留习惯静态图里用得不多。它的体积介于两者之间色度在垂直方向保留完整适合那种水平方向有大面积渐变的图。选 4:2:0 的场景就是绝大多数自然图像风景照、人像、商品实拍。这些图的色度细节本来就不丰富降采样带来的损失被掩盖得很好。有一个容易忽略的点从 4:2:0 的 JPEG 解码出来的图像再存成 4:4:4 并不会恢复丢失的色度信息。上采样只是插值原有的高频色度数据早就没了。所以“先存低质量再转高质量”这种做法完全是自欺欺人只会让文件变大而画质不变甚至更差。4. 上手实操Python 与命令行里把 JPEG 写对4.1 Pillow 参数逐条拆解Pillow 是最常用的入口但它的参数文档写得比较简略很多行为需要实测才能确认。下面这段是推荐的写法from PIL import Image im Image.open(source.png).convert(RGB) im.save( output.jpg, formatJPEG, quality85, # 量化表缩放1-100 subsampling1, # 04:4:4, 14:2:2, 24:2:0 optimizeTrue, # 生成最优霍夫曼表通常省 3%-8% progressiveFalse, # 渐进式首屏体验好但解码稍慢 dpi(72, 72), # 写入 JFIF 密度信息 )逐个说清楚。quality就是前面那套缩放公式的输入范围 1 到 100传 0 或负数会被当作 1 处理。subsampling必须显式指定别依赖默认值——默认的 -1 会跟随 quality 自动切换行为不可预测。optimizeTrue会让编码器先扫一遍所有系数统计分布生成针对这张图最优的霍夫曼表对大图能省 5% 左右代价是编码时间增加批量处理时值得开。progressiveTrue生成渐进式文件浏览器加载时先显示模糊轮廓再逐步清晰对慢网络下的首屏体验有帮助。但它有两个代价一是文件通常比 baseline 大 2% 到 5%二是解码时需要更多内存驻留中间状态在低端设备上可能导致解码变慢。社交平台的图片流用渐进式体验更好图标和缩略图没必要。dpi这个参数很多人不写结果 JFIF 头里的密度信息是默认值 1×1某些打印软件读出来会当成 1 DPI 处理尺寸算得离谱。做导出功能时最好显式写上。还有一个坑im.convert(RGB)这一步看似无害但如果原图带 alpha 通道转换时透明区域会变成黑色因为 Pillow 默认用黑色填充。想控制背景色得先建一张纯色底图再粘贴上去if im.mode in (RGBA, LA, P): im im.convert(RGBA) bg Image.new(RGB, im.size, (255, 255, 255)) bg.paste(im, maskim.split()[-1]) im bg4.2 OpenCV 与 ImageMagick 的差异点OpenCV 的接口是另一套参数体系cv2.imwrite用列表传参import cv2 img cv2.imread(source.png) # 注意默认读成 BGR 三通道 cv2.imwrite( output.jpg, img, [int(cv2.IMWRITE_JPEG_QUALITY), 90, int(cv2.IMWRITE_JPEG_OPTIMIZE), 1, int(cv2.IMWRITE_JPEG_PROGRESSIVE), 0], )第一个差异是BGR 顺序。cv2.imread读进来的通道顺序是蓝绿红如果你直接用 matplotlib 显示会发现颜色全反了需要手动转 RGB。保存时 OpenCV 会自己转回正确顺序所以存出来的 JPEG 颜色是对的但在处理管线里做颜色运算时顺序错了就会出大问题。第二个差异是quality 默认值是 95不是很多人以为的 85。这意味着不显式指定参数时OpenCV 存出来的文件会比 Pillow 默认75大不少。这也是“同一张图换个库体积突然翻倍”的常见原因。第三个差异是 OpenCV 不直接暴露 subsampling 参数底层用的是库默认值通常是 4:2:0。想精确控制采样因子得绕到别的库或者用命令行工具。ImageMagick 的命令行写法最直观magick source.png -sampling-factor 4:2:0 -quality 85 -strip -interlace none output.jpg-strip会清掉所有元数据和嵌入缩略图经常能省几十 KB——手机拍的照片里 EXIF 缩略图有时能占到 20 KB 以上对于缩略图列表页这种场景完全是浪费。-interlace none关掉渐进式-interlace JPEG打开。-define jpeg:extent200KB是个很好用的参数它会自动搜索一个能满足体积上限的 quality 值适合“上传必须小于 200 KB”这类硬性约束场景。注意它是二分搜索会多跑几轮编码批量处理时耗时翻倍。4.3 一个可复用的批量压缩脚本实际项目里很少单张处理下面这个脚本是我一直在用的版本把前面提到的坑都规避掉了from pathlib import Path from PIL import Image, ImageOps TARGET_LONG_EDGE 1600 QUALITY 85 SUBSAMPLING 2 # 4:2:0商品实拍够用 def process(src: Path, dst: Path): with Image.open(src) as raw: im ImageOps.exif_transpose(raw) # 按 EXIF 方向摆正 if im.mode ! RGB: im im.convert(RGB) w, h im.size long_edge max(w, h) if long_edge TARGET_LONG_EDGE: ratio TARGET_LONG_EDGE / long_edge im im.resize( (round(w * ratio), round(h * ratio)), Image.LANCZOS, ) im.save( dst, formatJPEG, qualityQUALITY, subsamplingSUBSAMPLING, optimizeTrue, progressiveTrue, ) for f in Path(input).glob(*.png): out Path(output) / (f.stem .jpg) process(f, out) print(f{f.name}: {out.stat().st_size / 1024:.0f} KB)ImageOps.exif_transpose这一步非常关键。手机拍照时感光元件是横向的竖拍照片会在 EXIF 里写一个旋转标记实际像素数据还是横的。如果直接压缩不处理方向很多老软件不认 EXIF 标记照片就会横躺着显示。用exif_transpose把旋转真正应用到像素上再配合后续可能的-strip就能保证任何环境下方向都正确。缩放用Image.LANCZOS而不是默认的最近邻或双线性。LANCZOS 是带窗的 sinc 插值缩小时能有效抑制摩尔纹和锯齿代价是慢一点。批量处理几百张图时这个开销完全可以接受。注意不要让缩放和 JPEG 压缩的顺序反过来。先压后缩意味着你在有损数据上再做插值压缩伪影会被放大正确顺序永远是先做所有几何操作和颜色调整最后一步才编码成 JPEG。4.4 无损操作jpegtran 的正确用法有些操作不需要重新编码就能完成比如 90 度旋转、水平翻转、裁剪到块边界。这类操作要用专门的工具因为普通编辑软件的流程是“解码 → 改像素 → 重新编码”中间那次解码-编码就是一次完整的世代损失。jpegtran -rotate 90 -copy all input.jpg rotated.jpg jpegtran -crop 800x60000 -perfect input.jpg cropped.jpg-copy all表示保留所有元数据段不加这个参数默认会丢掉 EXIF 和 ICC 配置。-perfect表示只在裁剪边界刚好对齐 MCU最小编码单元4:2:0 下是 16×16 像素时才执行否则报错退出。这个约束听起来麻烦但保证了输出文件没有任何重新编码画质完全一致。如果裁剪边界没对齐 MCU工具会退回到重新编码模式。这时候有个-trim参数可以自动调整到最近的 MCU 边界代价是裁出来的尺寸和你要求的不完全一样。做头像裁剪这类对尺寸精度要求高的场景建议还是走完整解码-裁剪-编码流程一次性完成不要试图用无损工具拼接多次操作。5. 典型翻车现场与排查速查表5.1 视觉伪影块效应、振铃、蚊式噪声、色度溢出JPEG 的伪影有明确的成因认出来就能对症下药。块效应表现为 8×8 的方格状边界尤其在平滑渐变区域最明显。成因是相邻块独立做 DCT量化后各自的 DC 值有微小偏差解码时块边界出现台阶。quality 越低越明显。缓解办法是提高 quality、改用更大的块标准不支持需要换格式或者在解码后做去块滤波。振铃效应表现为物体边缘外侧出现一圈波纹状亮暗交替。成因是高频系数被量化归零后DCT 基函数无法完美重建陡峭边缘产生吉布斯现象。文字和高对比度边缘最容易中招。缓解办法是降低压缩强度或者对文字类图像直接用无损格式。蚊式噪声是振铃的一种表现为平滑区域靠近边缘处出现细小的噪点飞舞。同样是高频丢失导致的。色度溢出表现为高饱和颜色区域的颜色“渗”到相邻区域比如红色文字旁边出现粉色晕影。根因是 4:2:0 下色度分辨率只有四分之一上采样时插值扩散开了。解法是改用 4:4:4或者干脆换格式。现象主要成因优先处理方式8×8 方格边界DC 量化偏差 低 quality提高 quality 到 85 以上边缘波纹高频系数丢失提高 quality或换 PNG平滑区色带低频量化过粗提高 quality检查量化表红色/品红糊成一片4:2:0 色度下采样改 4:4:4文字边缘彩色噪点色度下采样 振铃叠加换 PNG 或 4:4:4 q≥90整图发灰发暗色彩配置文件丢失保留 ICC或先转 sRGB5.2 世代损失与元数据陷阱世代损失是最隐蔽的问题因为单次压缩看不出来反复压缩才暴露。每解码-编码一轮量化误差就叠加一次而且误差是朝着量化格点收敛的——高频细节被反复抹平最终图像会变得越来越“塑料感”纹理消失、边缘发软。我做过一个实验同一张图连续存 10 次 quality90第一次 2.4 MB第十次 1.7 MB体积在降但画质明显变糊。更糟的是伪影会逐轮累积块效应从轻微变成明显。所以工程规范里应该有明确一条同一张图在流水线里的 JPEG 编码次数必须为一。所有中间环节用无损格式传递。元数据陷阱主要有两个方向。一是该保留的丢了ICC 色彩配置文件被 strip 掉导致广色域图片在普通屏幕上显示得灰扑扑EXIF 方向标记丢失照片躺倒。二是该丢的没丢手机照片的 EXIF 里可能包含拍摄地点等隐私信息对外发布前必须清理嵌入的缩略图白白占用体积。处理原则对外发布的图片用-strip清干净再手动写回必要的方向信息和 sRGB 标记内部流转的图片保留完整元数据。5.3 兼容性与色彩问题CMYK 模式的 JPEG在浏览器里经常显示成反色或者颜色完全错乱。原因是 CMYK JPEG 有两种互不兼容的存储约定一种来自 Adobe 的约定带 APP14 标记一种是普通约定不同解码器支持的都不一样。Web 场景下的图片一律转成 sRGB 再存别把印刷用的 CMYK 文件直接扔上去。渐进式文件在部分老设备上解码失败表现为图片只显示上半部分或者一片灰。这类问题在嵌入式设备和老旧浏览器上偶有出现。如果你的用户群里有大量低端设备保守起见用 baseline。超大尺寸图片比如单边超过 65535 像素在 baseline JPEG 里根本存不了因为 SOF 段里的宽高字段是 16 位。遇到全景图拼接这类场景要么改用支持更大尺寸的格式要么分块存储。6. 选型决策什么时候坚决不用 JPEG6.1 五种格式横向对比把 JPEG 放进整个格式家族里看边界才清晰格式压缩类型透明通道典型体积编码速度解码兼容性最适合JPEG有损 DCT不支持小快极好照片、复杂色彩渐变PNG无损 DEFLATE支持大照片类 3-8 倍中等极好截图、图标、文字图、需要透明WebP有损 无损双模式支持有损比 JPEG 小 25% 左右中等良好现代浏览器Web 场景综合替代AVIF有损 AV1 帧内支持比 JPEG 小 50% 左右慢中等追求极致体积的静态图HEIF有损 HEVC 帧内支持与 AVIF 接近慢中等移动端相册、连拍序列从这张表能看出 JPEG 的位置它不是压缩率最高的也不是功能最全的但它在“兼容性 × 解码成本 × 编码速度”这个三维空间里仍然是最稳的。选型的核心不是比谁先进而是看你的链路里哪一环是瓶颈。6.2 几条可以直接抄的判断阈值下面这些阈值是我在实际项目里反复验证过的可以直接拿去用看内容类型。如果图里有文字、线条、图标、纯色块一律不用 JPEG。这类内容的高频信息集中在锐利边缘正好是 DCT 量化最伤的地方而且无损格式对这类图的压缩率反而很高大片纯色DEFLATE 压得极好。看是否需要透明。需要 alpha 通道就直接排除 JPEG没有讨论余地。宁可体积大一点用 PNG 或 WebP。看是否需要多次编辑。工作流里只要涉及超过一次的保存动作中间格式必须无损。JPEG 只出现在最终输出环节。看体积约束的松紧。如果一张图压缩到 100 KB 以内就能满足需求那 JPEG 在 q75 左右大概率能搞定没必要引入新格式。如果约束是 30 KB 以内且画质不能妥协才值得考虑换格式并承担兼容性成本。看解码端能力。服务端渲染、老设备、嵌入式屏幕这些场景的 JPEG 硬解支持几乎是白送的换格式意味着纯软件解码CPU 和电量开销都会上升。这种情况下用 JPEG 换来的稳定性收益往往大于省下的那点带宽。看 CDN 和工具链。有些图片处理服务对非 JPEG 格式的转换、缩放支持不完整或者会偷偷转码回 JPEG。上线前一定要用真实链路验证一次别只在本地测试就下结论。还有一个实际经验不要为了省 20% 体积给所有图统一换格式。正确的是分层策略——首屏大图用新格式 JPEG 回退列表缩略图统一用 JPEG体积本来就小收益不明显图标用 SVG 或 PNG。按重要性分层比一刀切有效得多。7. 我在实际项目里踩出来的几条经验有一件事我印象很深。某个图片列表页做了格式升级把 JPEG 换成了压缩率更高的格式结果页面加载时间没降反升。排查了半天发现是解码开销的问题图片从 200 KB 降到 110 KB网络传输省了 90 KB但每张图解码多花了 8 毫秒一屏 30 张图就多了 240 毫秒反而拖慢了首屏。这个教训让我明白图片优化的目标函数不是体积是端到端的渲染时间中间任何一个环节的变化都要实测验证。另一条经验关于 quality 的取值。我现在的默认配置是列表缩略图 q78、正文配图 q85、商品主图 q88、需要精修展示的 q92超过 92 一律不允许出现在代码库里因为收益太小而体积增长太快。这个分档写进团队的配置常量里任何人要改都要提交理由避免有人遇到显示问题时第一反应就是把 quality 拉到 100。还有一个关于测试的习惯。压缩参数调完之后别只在缩略图里看效果一定要把图放大到 100% 甚至 200% 检查两个地方平滑渐变区域有没有色带高对比边缘有没有彩色镶边。这两个位置是最早暴露问题的看这两处没问题其他地方基本就安全了。检查的时候最好在 sRGB 环境下看广色域显示器上看到的色带可能比普通用户看到的更淡容易误判。