ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

渐进式JPEG和基线式,我拿同一张图比了体积和解码

渐进式JPEG和基线式,我拿同一张图比了体积和解码 摘要做活动页的同事前几天问我导出 JPG 时那个「渐进式」到底要不要勾。网上两种说法我都见过。一种说渐进式文件更小还能在网慢时先糊后清。另一种说它解码更费劲放到手机上反而拖后腿。我以前一直凭感觉乱勾这回干脆拿同一张图两种各存一份挨个比了四样东西。比的是体积、解码耗时、只收到一部分数据时能画出什么、手边的工具各自导出哪一种。先说结论。同样质量下渐进式小了 3.9%–6.3%。它的解码耗时是基线式的约 2.3–2.6 倍。好在两边都只是毫秒级。渐进式真正占便宜的地方在网慢的时候。模拟只收到一成数据时渐进式已经能画出整张偏软的图基线式只画出最上面一条。麻烦在于我测的三个浏览器内核用 canvas 导出的全是基线式图映导出的 JPG 也一样。想要渐进式就得交给专门的编码工具。好在已有的基线式文件用 jpegtran 就能改排成渐进式不用重新压也不动一个像素。我最后的做法是首屏那几张大图转一百来像素宽的小缩略图不转。版本声明数据是 2026-09-30 跑的。机器是一台装着 macOS 26.5.2 的 16 GB 内存 Apple M4 Mac。存 JPG 主要用的是底下跑着 libjpeg-turbo 的 Pillow 11.3存的时候都开了 optimize。命令行工具是 Homebrew 装的 libjpeg-turbo 3.2.0 里自带的 cjpeg、djpeg 和 jpegtran外加 macOS 自带的 sips。浏览器用了 Chromium 149、Firefox 151 和 WebKit 26.5 三个开源构建。它们跟日常装的正式版 Chrome、Firefox、Safari 不是一回事。文里说到内核的地方只管这三个构建正式版 Safari 上是什么情况我没验过。解码耗时是在 Chromium 149 里用 createImageBitmap 重复量 7 次取的中位数。KB 按 1024 字节算表里的字节数都是照着文件大小原样抄下来的。适用边界样本只有两张网上找的图不是我拍的也不是我做的。一张是 Wikimedia Commons 上的一张风景照。原图是 5472×3648我把它缩到 2736×1824 当成网页里的一张大图来用。另一张是网上找的「2026年国庆节放假通知」模板图。这张 1242×2688 的图出自稿定设计的模板红底上有插画、日历和好几行字。每张都存了质量 75 和 85 两档每档各一份基线式和渐进式。「只收到一部分数据」那一章是拿解码器模拟的做法是把文件截断以后再解码不是在浏览器里实拍的。我试过在浏览器里限速截屏可每次截到的都是整张图下完之后的画面。中间过程一次也没拍到。因为这个文里不会写渐进式在浏览器里让首屏快了几秒。手机上的解码耗时我也没测。文章目录一、渐进式JPEG和基线式有什么区别1.1 勾不勾渐进式我一直凭感觉1.2 两种文件解出来的像素完全一样1.3 区别只在数据分几趟存二、怎么看出一张JPG是哪一种2.1 文件头里的SOF标记说了算2.2 渐进式文件被拆成了10段三、换成渐进式能省多少体积3.1 同样质量下渐进式小了一截3.2 基线式开了优化也能省下一部分3.3 缩略图太小反而会变大四、渐进式解码会慢多少4.1 大图解码慢了约2.5倍4.2 40张缩略图多花的时间不到一帧五、网速慢时渐进式能先显示什么5.1 只收到一成数据就能看到整张图5.2 收到一半时已经接近原图5.3 放假通知模板上差距更大六、哪些工具能导出渐进式JPEG6.1 三个浏览器内核只导出基线式6.2 命令行工具加一个参数就能导出6.3 jpegtran转换不重压也不丢像素七、什么情况值得换成渐进式7.1 首屏大图值得换7.2 小缩略图不用换7.3 已有的图可以批量无损转换八、适用边界与风险九、测这两种JPEG容易踩哪些坑9.1 截断解码不等于浏览器实拍9.2 比体积前先把优化选项对齐9.3 解码耗时要多跑几轮十、还有哪些没弄清楚参考资料一、渐进式JPEG和基线式有什么区别1.1 勾不勾渐进式我一直凭感觉我在一家内容公司做技术岗写脚本、搭内部小工具什么都沾一点。我是中文系转过来的图片格式这些都是边用边学。活动页和专题页上的大图常常要我帮忙压那个「渐进式」选项我见过很多回。有的工具把它藏在高级设置里有的直接默认勾上。我以前处理得很随便默认勾了就勾着。没勾也懒得去点。前几天做活动页的同事问我到底要不要勾我张嘴就想说「勾上吧听说更小」话到嘴边才发现自己根本说不出小多少。网上两派说法我都看过。支持的一派说渐进式文件更小网慢时先出一张糊图再慢慢变清楚比基线式从上往下一截一截刷出来要好看。反对的一派说它解码要多跑好几趟在手机上费 CPU 反而拖后腿。两边说得都挺像回事可我没见过谁拿同一张图把这几项一起摆出来。这次我就按这个思路拿同一张图、同一个质量只换存法挨个量一遍。1.2 两种文件解出来的像素完全一样动手之前我有个误会我一直以为渐进式是另一种压法画质会跟基线式有点不同甚至更糊一点。第一件事我就去验这个把风景照质量 85 的两份文件都解码成像素再一个一个比。最大差值是 0每个像素的 RGB 都一模一样。也就是说在同一个质量下两种文件装的是同一张图。画质损失在压缩那一步就定了存成哪种排法都不会再去动它。这一点对后面的判断很要紧既然画质完全一样要比的就只剩体积、解码速度和加载过程这三件事。哪个更清楚这事不用再纠结了。1.3 区别只在数据分几趟存这部分我是照着格式规范和别人的讲解理解的。讲得会土一点。JPEG 会先把图切成一个个 8×8 的小方块再把每个方块换算成一组系数。排在前面的几个系数管这一块大概是什么颜色和明暗排在后面的管细节和边缘。基线式按顺序一块一块地存一块的全部系数存完了再存下一块。浏览器收到多少数据就只能画出多少块画面就这样从上往下一截一截长出来。渐进式把这个顺序换了过来先把所有方块的「大概颜色」存一遍然后一趟一趟地往上补细节。收到第一趟就能画出整张图的轮廓。只是画面很糊。后面每多收一趟画面就清楚一点。两边存的是同一批系数只有先后顺序不同。上一节的像素为什么一样在这里就说得通了。至于体积为什么会差一点我的理解是换了顺序以后同类的系数挨在一起更好压。这是我从讲解里看来的编码细节没有自己去验。二、怎么看出一张JPG是哪一种2.1 文件头里的SOF标记说了算两种文件打开以后长得一样光看文件名和图片查看器是分不出来的。能分清的只有文件头。一段一段拼起来的 JPEG 文件每段开头都有两个字节的标记。其中叫 SOF 的那一段意思是「帧开始」里面记着宽高和编码方式。标记是 FF C0 的是基线式。FF C2 的是渐进式。我在规范里查到这个标记以后写了个小脚本顺着段往下读。扫描段就是上一章说的那个「一趟」我顺手把它的个数也数了。# 看一张 JPG 是基线式还是渐进式顺着段标记往下走记下 SOF 类型和扫描段个数importsysdefkan_jpg(lujing):shujuopen(lujing,rb).read()leixing,saomiao,i没找到,0,2whilei4len(shuju):biaojishuju[i1]ifbiaoji0xD9:breakchangdushuju[i2]8|shuju[i3]ifbiaojiin(0xC0,0xC1):leixing基线式elifbiaoji0xC2:leixing渐进式i2changduifbiaoji0xDA:saomiao1# 扫描数据里的 FF 后面只会跟 00 或复位标记碰到别的才是下一个段whilenot(shuju[i]0xFFandshuju[i1]!0andnot0xD0shuju[i1]0xD7):i1returnleixing,saomiao,len(shuju)forfinsys.argv[1:]:lx,sm,zjkan_jpg(f)print(f{f.split(/)[-1]:28s}{lx}扫描段{sm:2d}{zj:,}字节)代码说明开头两个字节 FF D8 是固定的文件起始标记脚本从第 3 个字节开始读。每读到一段先看它是不是 SOF 再按段头里记的长度跳到下一段。标记是 FF DA 的扫描段比较特殊。段头后面紧跟着的压缩数据有多长段头里没写只能一个字节一个字节往后找。压缩数据里碰巧出现的 FF 后面会被编码器补上一个 00这种不算新段。FF D0 到 FF D7 这几个复位标记也不算新段。碰到别的 FF 开头的两个字节才是下一段真正的开始。日常很少见的扩展顺序编码 FF C1 我也算进了基线式这边。我拿这次存的八份文件和那张 Wikimedia 原图跑了一遍输出是这样的。landscape_q75_base.jpg 基线式 扫描段 1 844,255 字节 landscape_q75_prog.jpg 渐进式 扫描段 10 811,350 字节 landscape_q85_base.jpg 基线式 扫描段 1 1,098,272 字节 landscape_q85_prog.jpg 渐进式 扫描段 10 1,048,255 字节 template_q75_base.jpg 基线式 扫描段 1 532,106 字节 template_q75_prog.jpg 渐进式 扫描段 10 502,023 字节 template_q85_base.jpg 基线式 扫描段 1 754,005 字节 template_q85_prog.jpg 渐进式 扫描段 10 706,842 字节 landscape.jpg 基线式 扫描段 1 12,063,992 字节2.2 渐进式文件被拆成了10段输出里最显眼的是扫描段那一列。基线式全是 1 段所有数据一趟存完。渐进式全是 10 段也就是分了 10 趟。后面我用 cjpeg 和 jpegtran 生成的渐进式也都是 10 段看来 libjpeg-turbo 默认就是这么排的。我猜是亮度和色度分开补再从粗到细分成几档具体每一趟装的是什么我没去细看。最后一行那张 12,063,992 字节的 Wikimedia 原图也是一趟存完的基线式。我从 Wikimedia Commons 下的三张照片里风景原图和小狗图是基线式另一张是渐进式。我原以为放大图的站点会更爱用渐进式。三张里只占了一张。这个脚本后面还会反复用到我判断某个工具到底导出了哪一种靠的就是它。比起工具界面上写的选项我更信文件头。三、换成渐进式能省多少体积3.1 同样质量下渐进式小了一截先把口径说清楚两张样本各存质量 75 和 85 两档。同一档里基线式和渐进式只差 progressive 这一个开关其他参数都一样也都开了 optimize。下面是四组结果。样本质量基线式字节渐进式字节渐进式小了风景照75844,255811,3503.9%风景照851,098,2721,048,2554.6%放假通知模板75532,106502,0235.7%放假通知模板85754,005706,8426.3%四组里渐进式都小了 3.9%–6.3%。放假通知模板比风景照省得多一点。两个质量档都是这样。同一张图上质量 85 也比 75 多省一点。这个量说大不大一张一兆出头的大图能省下四五十 KB。页面上只有一两张大图的话对加载的影响就很有限。要是一个页面挂着几十张大图攒起来就不少了。画质既然完全一样这四五个百分点就等于白捡。只是捡得并不多没有网上说的那么夸张。这张图左右两半要分开看左半边是以 KB 为单位的文件体积右半边是以毫秒为单位的解码耗时。灰色柱子是基线式。蓝色柱子是渐进式。每边从左到右依次是风景照质量 75、风景照质量 85、模板质量 75 和模板质量 85。左边每组的蓝柱只比灰柱矮一点点右边的蓝柱却比灰柱高出一倍多。体积省得少和解码慢得多这两件事在图上一眼就能看出来。不过右边纵轴最高才 28.3 毫秒这个「慢」要不要紧得放到第四章跟一帧的时间一起看。3.2 基线式开了优化也能省下一部分这里我绕了个弯。最开始我用 cjpeg 的默认参数存了一份基线式再加 -progressive 存了一份渐进式。一比小了 6.0%比上面表里风景照质量 85 的 4.6% 多出一截。我还以为自己测出了什么新东西后来才想起 cjpeg 默认是不开 optimize 的。optimize 做的事是按这张图自己的数据重新算一套哈夫曼编码表。不开的时候用的是规范里给的那套通用表。我查了一下libjpeg 在渐进式模式下不管你开没开 optimize 都会自己把这张表算好。数字也对得上。cjpeg 只加 -progressive 不加 -optimize 出来的文件跟 Pillow 开了 optimize 的渐进式一个字节都不差。拿一个没开优化的基线式去跟渐进式比省下来的量里就混进了编码表的功劳。拆开看的话风景照质量 85 用 cjpeg 默认参数存是 1,115,122 字节。只加 -optimize 是 1,098,272 字节小了 1.5%。再换成渐进式是 1,048,255 字节。那 6.0% 里有 1.5% 是基线式自己开个优化就能拿到的剩下的才是换排法挣来的。第六章还会看到有的浏览器内核导出基线式时就没开这个优化它的文件换成渐进式能省得更多。3.3 缩略图太小反而会变大网上还有一种说法是小图存成渐进式反而更大。我想知道这个「小」到底要多小于是把两张样本缩成 64 到 1280 像素宽的七档。每档都用质量 85 并开着 optimize 各存一份基线式和渐进式。# 同一张图缩成不同宽度各存一份基线式和渐进式都开 optimize、质量 85看体积差随尺寸怎么变importsys,iofromPILimportImageasTudefcun(im,jianjin):huancunio.BytesIO()im.save(huancun,JPEG,quality85,optimizeTrue,progressivejianjin)returnhuancun.tell()foryuaninsys.argv[1:]:daTu.open(yuan).convert(RGB)forkuanin(64,96,120,160,320,640,1280):xiaoda.resize((kuan,round(da.height*kuan/da.width)),Tu.LANCZOS)ji,jiancun(xiao,False),cun(xiao,True)print(f{yuan.split(/)[-1]:18s}{xiao.width}×{xiao.height:5d}基线{ji:8,}渐进{jian:8,}差{(jian-ji)/ji:.1%})代码说明cun 这个函数把图存进内存里的一个缓冲区 huancun。存完看缓冲区写到了第几个字节就是文件大小。不用真的落盘。缩图用的是 LANCZOS 并按原图的比例算高度。输入用还没压成 JPG 的那两份参考 PNG免得先压一遍再缩多带进一层损失。最后一列是渐进式比基线式多了还是少了。正数就是变大。landscape_ref.png 64×43 基线 1,506 渐进 1,678 差 11.4% landscape_ref.png 96×64 基线 2,594 渐进 2,721 差 4.9% landscape_ref.png 120×80 基线 3,876 渐进 3,916 差 1.0% landscape_ref.png 160×107 基线 6,975 渐进 6,824 差 -2.2% landscape_ref.png 320×213 基线 25,815 渐进 24,525 差 -5.0% landscape_ref.png 640×427 基线 95,723 渐进 90,194 差 -5.8% landscape_ref.png 1280×853 基线 321,678 渐进 304,951 差 -5.2% template_ref.png 64×139 基线 4,354 渐进 4,422 差 1.6% template_ref.png 96×208 基线 8,392 渐进 8,243 差 -1.8% template_ref.png 120×260 基线 12,228 渐进 11,870 差 -2.9% template_ref.png 160×346 基线 19,942 渐进 19,150 差 -4.0% template_ref.png 320×693 基线 62,509 渐进 59,578 差 -4.7% template_ref.png 640×1385 基线 202,015 渐进 190,903 差 -5.5% template_ref.png 1280×2770 基线 746,301 渐进 705,401 差 -5.5%这个说法是真的风景照缩到 160 宽时渐进式还小 2.2%到 120 宽就反过来大了 1.0%。96 宽大了 4.9%64 宽大了 11.4%。我又往下补了两档48 宽大了 25.0%32 宽大了 36.8%。竖长的模板图在同样的宽度下像素多得多到 64 宽才开始变大96 宽还小 1.8%。看宽度不太准看文件大小更靠谱。两张图翻过来的地方都落在四千多到七千字节之间再小的文件存成渐进式就不划算了。我猜是渐进式每一趟都要带上自己的段头和编码表10 趟下来这些固定开销就有好几百字节。这些开销在大文件里摊薄了看不出来一两 KB 的小图就被它压着了。这个解释只是我按段结构推出来的。四、渐进式解码会慢多少4.1 大图解码慢了约2.5倍体积说完说解码。我在 Chromium 149 里用 createImageBitmap 把八份文件各从头解码 7 次取中间那个数。样本质量基线式解码 ms渐进式解码 ms风景照759.724.7风景照8511.128.3放假通知模板756.415.6放假通知模板857.519.2渐进式的解码耗时是基线式的约 2.3–2.6 倍笼统说就是解码慢了约 2.5 倍。这个倍数比体积那边的差距大得多也是反对派最常拿出来说的一条。可绝对值得一起看。最慢的风景照质量 85 那组是 28.3 毫秒比基线式多了十来毫秒。屏幕按 60 帧刷新的话一帧大约是 16.7 毫秒。一张 2736×1824 的大图多花的时间差不多就是一帧单独一张图眼睛是分不出来的。慢在哪里我也有个猜测基线式一块解完就可以扔掉。渐进式得先把整张图的系数都攒着每来一趟就把所有方块再过一遍最后才输出像素。多出来的时间大概主要花在这几趟上。这是我按原理推的。解码器的代码我没去翻。还有一点要说在前头。手机的芯片要比这台 M4 的 Mac 慢不少同样的图在手机上多花的时间会更长。我没测所以不好说长多少。4.2 40张缩略图多花的时间不到一帧单张大图不要紧那一屏几十张缩略图呢。我把风景照缩成 320×213 并用质量 85 存了基线式和渐进式各一份。然后在 Chromium 149 里连着解码 40 张算总耗时并取 7 轮的中位数。这一项我跑了两次。基线式两次是 8.7 和 8.3 毫秒渐进式是 19.6 和 16.8 毫秒。40 张加起来多出八九到十一毫秒。还是不到一帧。换来的体积是每张小 1,290 字节40 张一共五十来 KB。两次结果之间差了将近 3 毫秒这种毫秒级的数本来就晃得厉害。看个量级就行。真要算细账的话这点解码时间在电脑上基本可以忽略问题在于缩略图本来就小、下得也快。渐进式那个先糊后清的好处在它们身上几乎用不上多花的解码时间只换来体积上那一点点。五、网速慢时渐进式能先显示什么5.1 只收到一成数据就能看到整张图这一章讲的是渐进式真正的卖点也是我最想亲眼看到的。一开始我想在浏览器里直接拍下来。我在本地起了个服务把图片限速成每 100 毫秒吐 20 KB 再隔一段时间截一次屏。结果截到的画面要么是白的要么整张图已经下完。中间那段「半张图」一次都没拍到。原因我没找到也不想为这个花一整天。于是改成拿解码器模拟文件只留前 10%、25%、50%、75% 的字节。解码器按「数据不完整也尽量画」的方式解出来以后再跟原图比有多接近。比的是 PSNR一个衡量跟原图差多少的数。它的单位是 dB越大越接近原图。# 模拟「只下载到前面一部分」把 JPG 截到前 N% 字节让 Pillow 能画多少画多少再跟完整文件的参考图比 PSNRimportsys,ioimportnumpyfromPILimportImageFile,Image ImageFile.LOAD_TRUNCATED_IMAGESTrue# 数据不完整也不报错缺的部分照样出图defban_jie(lujing,baifenbi):quanopen(lujing,rb).read()duanquan[:len(quan)*baifenbi//100]returnnumpy.asarray(Image.open(io.BytesIO(duan)).convert(RGB),dtypenumpy.float64)deffenshu(jie,yuan):wucha((jie-yuan)**2).mean()return20*numpy.log10(255/numpy.sqrt(wucha))cankaonumpy.asarray(Image.open(sys.argv[1]).convert(RGB),dtypenumpy.float64)forfinsys.argv[2:]:hang[f{fenshu(ban_jie(f,p),cankao):6.2f}forpin(10,25,50,75,100)]print(f.split(/)[-1].ljust(24), .join(hang))代码说明关键是 LOAD_TRUNCATED_IMAGES 这一行。Pillow 默认碰到不完整的 JPG 会直接报错。打开它以后能解多少解多少没解出来的地方都填成灰色。基线式缺的是后面那几截也就是下面一片灰。渐进式缺的是后面几趟细节整张图都在只是糊。参考图用的是压成 JPG 之前的那份 PNG就算收到了 100% 的字节分数也不会是无穷大。它反映的是质量 85 本身的损失。fenshu 算的就是 PSNR用的是均方误差换算成 dB 的那个公式。第一个参数传参考图后面跟要比的 JPG。输出每行的五个数依次对应收到 10%、25%、50%、75% 和 100% 的字节。风景照和模板各跑了一次。landscape_q85_base.jpg 13.58 14.65 15.55 18.16 36.81 landscape_q85_prog.jpg 18.90 22.73 30.97 34.07 36.81 template_q85_base.jpg 8.16 8.55 9.50 13.03 32.33 template_q85_prog.jpg 19.94 24.92 29.19 31.06 32.33先看风景照。收到 10% 的字节时基线式只画出最上面一条其余全是灰块PSNR 13.58。渐进式这时整张图都在只是偏软PSNR 18.90。只收到一成数据就能看到整张图。这就是「先糊后清」的意思。这张风景照拍的是雾天的雪地远处一排光秃秃的树前面是一大片芦苇和积雪。我把两张截断解码的结果并排看了看渐进式那张的树、芦苇和积雪都在颜色也对。只是芦苇的细秆糊成了一团像一张没对上焦的照片。基线式那张上面一条雾蒙蒙的天和树梢倒是清清楚楚可芦苇和雪地全是灰块根本看不出这是一张什么图。5.2 收到一半时已经接近原图再往后看差距越拉越大收到 25% 时渐进式是 22.73 对基线式的 14.65。收到 50% 时渐进式已经到了 30.97 dB接近看不出差别。基线式这时还只画出上面一部分只有 15.55。收到 75% 时渐进式是 34.07基线式是 18.16。两边都要收完 100% 才汇合到同一个 36.81这跟第一章验的像素完全一样也对得上。有一点要提醒一下。基线式的分数涨得这么慢并不代表它画出来的那部分糊它画出来的每一块都已经是最终的样子。是底下那一大片灰把平均分拉了下来。PSNR 算的是整张图的平均误差大片缺失在它眼里比整体变软要严重得多。用这个数比两种文件天然就对渐进式有利一点可换成人眼去看一张完整但偏软的图能提前告诉你这是什么。只有上半截的图做不到这一点我觉得这个判断方向没错。5.3 放假通知模板上差距更大放假通知模板上的差距比风景照还要大收到 10% 时模板的基线式只有 8.16渐进式是 19.94。收到 25% 时基线式 8.55 对渐进式 24.92已经差了十几个 dB。收到 50% 时渐进式到了 29.19基线式才 9.50。我猜是因为模板整张都是大红底。基线式没画出来的地方填成灰灰和红差得太远平均误差就被拉得更狠。风景照本来就是一大片灰白的雾天雪景填成灰色吃的亏小一点。这也说明 PSNR 的具体数值跟图的颜色有关。不同的图之间横着比没什么意义同一张图上两种排法竖着比才有意义。这张图是放假通知模板只收到 25% 字节时的样子三栏从左往右看。左边的基线式只画出了最上面一小截「放假通知」那行标题都没画全下面全是灰。中间是同样只收到 25% 的渐进式整张图已经出来了。标题、日历和底部的祝福语都认得出。细看比右边软一点。右边是拿来跟中间那栏对照的完整文件。放在网页上的话读者等到中间这个状态就知道这是一张放假通知了。要提醒的是这三栏是解码器模拟的结果。浏览器里实际显示成什么样还要看它多久重画一次、数据怎么分批到达这个我没拍到。六、哪些工具能导出渐进式JPEG6.1 三个浏览器内核只导出基线式搞清楚渐进式的好处以后下一个问题就是怎么拿到它。我们组的内部小工具有不少是在网页里用 canvas 导出图片的我就先看浏览器。我在三个内核里把风景照画到 canvas 上再用 toBlob 各导出质量 0.5、0.85 和 1 三档。再拿第二章的脚本读文件头九个文件全是一趟存完的基线式。canvas 的 toBlob 只收格式和质量两个参数根本没有地方告诉它要渐进式。下面是质量 0.85 那一档的三个文件。内核类型扫描段字节Chromium 149基线式11,098,746Firefox 151基线式11,115,122WebKit 26.5基线式11,916,407这三个数我对着前面的结果看了半天看出几件挺有意思的事。Chromium 导出的文件解码出来的像素跟 Pillow 质量 85 的基线式一个不差。大小只多了 474 字节多出来的是文件头里的一段 ICC 色彩配置。Firefox 导出的文件跟 cjpeg 默认参数存的那份一个字节都不差。也就是说它没开 3.2 节说的那个编码表优化。WebKit 导出的文件跟 macOS 自带的 sips 用质量 85 存的那份也是一个字节都不差。我猜它俩用的是系统里同一套编码。同样填 0.85 的时候 WebKit 的文件比 Chromium 大了七成多。它的质量刻度跟另外两个显然不是一回事。这个我没往下查。图映的 JPG 我也看了。前两天压那张红底海报时我用的是图映 ImgInghttps://imging.cn/在 Chromium 149 开源构建里用全默认参数导出过两份 JPG。这回我把这两份翻出来读了文件头都是基线式。图映转 JPG 走的是浏览器自带的编码浏览器只给基线式它导出来的自然也是基线式。想在浏览器里一步到位拿到渐进式至少在我测的这几样东西上走不通。6.2 命令行工具加一个参数就能导出浏览器导不出来就得交给专门的编码工具我手边能直接用的有两样。一样是 libjpeg-turbo 自带的 cjpeg加一个 -progressive 就是渐进式。另一样是 Python 的 Pillow保存时传 progressiveTrue 就行。两边导出来的风景照质量 85 都是 1,048,255 字节的 10 段渐进式一个字节都不差。这不奇怪。Pillow 底下用的就是 libjpeg-turbo同一套代码出同一个文件。sips 默认存出来的是基线式它有没有渐进式的开关我没去翻。要在服务端批量处理的话这两样随便挑一个都行。写脚本的用 Pillow。图省事的用 cjpeg。只是它们都要从像素重新编一遍已经压好的 JPG 再压一次会多一轮损失。这就引出了我这次最想安利的一个工具。6.3 jpegtran转换不重压也不丢像素jpegtran 也是 libjpeg-turbo 自带的。它不解码成像素就直接在压缩后的系数上动手。加 -progressive 就是把系数换个顺序重新排成渐进式。我先拿 Pillow 存的基线式转了一份。出来的文件是 1,048,255 字节跟直接编码的渐进式连 md5 都一样。反过来把渐进式转回基线式也跟原来那份基线式的 md5 一样。两种排法之间可以来回倒而且一点都不走样。这跟第一章讲的原理完全对得上。接着我把三个内核导出的 0.85 文件各转了一遍解码出来的像素跟转之前比最大差都是 0。Chromium 那份小了 4.6%。Firefox 那份小了 6.0%因为它原本没开的编码表优化在转的时候顺带补上了。WebKit 那份小了 10.9%是三个里省得最多的。这里我踩了个小坑一开始我照着网上的例子写了表示不复制任何附加信息的 -copy none。转完才发现 Chromium 塞在文件头里的那段 ICC 色彩配置也被一起扔了。照片里的 EXIF 同样会被扔掉要保留就得换成 -copy icc 或者 -copy all。换成 -copy icc 以后 Chromium 那份是 1,048,729 字节照样小 4.6%。Wikimedia 那张一千二百多万字节的原图我也用 -copy all 转了。它从 12,063,992 字节变成 11,436,051 字节小了 5.2%。七、什么情况值得换成渐进式7.1 首屏大图值得换把前面的账合起来我的判断是首屏大图值得换。几百 KB 以上的大图换成渐进式能省 4%–6% 的体积。原图要是 Firefox 或 WebKit 内核这类没开优化的编码器出的省得还更多。解码多花的时间在电脑上差不多是一帧。用户感觉不到。最大的好处在网慢的时候读者只收到一成数据就能先看到整张图的样子不用盯着一片空白等它从上往下刷。活动页头图、文章题图和商品详情页的大图都属于这一类。这类图数量不多一个页面也就一两张转起来花不了什么工夫。我后来就是这么跟同事说的。首屏那几张大图勾上别的先别管。7.2 小缩略图不用换小缩略图我不换。一是省不下什么第三章测到四千多到七千字节以下的小图存成渐进式反而会变大。二是换了也没好处缩略图本来就只有几 KB 到几十 KB网再慢也是一眨眼就下完了。先糊后清那个过程根本来不及发生。一屏几十张缩略图全换成渐进式电脑上多花的解码时间不到一帧。体积也就省几十 KB折腾一遍的意义不大。要说例外就是那种三百来像素宽、二三十 KB 的中等缩略图。它们换了也能小 5% 左右量特别大、流量特别贵的话可以考虑。我自己的活里没有这种量就没动它们。7.3 已有的图可以批量无损转换手上已经压好的一堆基线式 JPG 用 jpegtran 批量转最省事。我写了个小脚本一个文件夹进去转完逐像素核对没变小的就留原文件。#!/bin/zsh# 把一个文件夹里的基线式 JPG 无损改排成渐进式不重新压改完逐像素核对变大的就留原文件mkdir-pjianjinfortuinyuan/*.jpg;doxinjianjin/${tu:t}jpegtran-copyall-progressive-outfile$xin$tuif!cmp-s(djpeg-pnm$tu)(djpeg-pnm$xin);thenecho$tu像素对不上跳过;rm$xin;continue;fiqian$(stat-f%z $tu);hou$(stat-f%z $xin)((houqian)){cp$tu$xin;echo${tu:t}转完没变小留原文件;continue;}printf%-22s %9d - %9d %.1f%%\n${tu:t}$qian$hou$(((hou-qian)*100.0/qian))done代码说明原图放在 yuan 文件夹里不动转好的放进 jianjin 文件夹。-copy all 会把 EXIF 和色彩配置都带上这是上一章踩坑以后改的。核对那一步用 djpeg 把转换前后的两个文件都解成原始像素再用 cmp 逐字节比。只要有一个字节不一样就跳过不留转坏的文件。stat -f%z 是 macOS 的写法Linux 上要换成 stat -c%s。变大的文件直接把原图拷过去这样 jianjin 文件夹里始终是一套齐全的图拿去替换时不会少文件。我往 yuan 里放了五张图。两张是 Chromium 和 WebKit 导出的风景照一张是前面那份图映导出的红底海报。另外两张是 320 宽和 64 宽的缩略图跑出来是这样的。fengjing_chromium.jpg 1098746 - 1048729 -4.6% fengjing_webkit.jpg 1916407 - 1707550 -10.9% haibao_tuying.jpg 147563 - 133870 -9.3% suolue_320.jpg 25815 - 24525 -5.0% suolue_64.jpg 转完没变小留原文件五张里四张变小64 宽那张缩略图转完没变小就按规则留了原文件。那张红底海报小了 9.3%比两张照片省得都多。我猜是它大块都是纯色基线式的通用编码表对它特别不合适具体原因我没深究。脚本跑一遍就几秒钟也不改画质出了问题删掉 jianjin 文件夹就当没发生过。我现在把这个脚本放在做活动页素材的文件夹旁边首屏那几张图交出去之前都过一遍。八、适用边界与风险先说样本。我只测了一张风景照和一张放假通知模板外加一张图映导出的红底海报。体积差的百分比换一张图就会变。不能当成所有图都这样。解码耗时只在一台 M4 的 Mac 上的 Chromium 149 里量过手机和别的内核都没量。Firefox 151 和 WebKit 26.5 我只测了它们导出的是哪一种。它们解码渐进式要多久我没测。三个浏览器都是开源构建结论不能直接套到正式版的 Chrome、Firefox 和 Safari 上。再说加载过程。第五章全是解码器模拟浏览器实际怎么显示我没拍到。有的浏览器可能要攒够一定的数据才画第一版那样看到糊图的时间会比模拟的晚。还有三处风险要留意。一是 jpegtran 用 -copy none 会把 EXIF 和色彩配置一起扔掉。带 ICC 的图颜色可能会变我现在都用 -copy all。二是图片上线以后要是还会被别的服务重新压缩一次排法可能又被改回基线式。这个我没办法测最稳的办法是上线后把线上那个文件下载下来用第二章的脚本读一下文件头。三是渐进式解码要把整张图的系数先攒在内存里。特别大的图会不会吃更多内存我没量做批量处理的服务要自己盯一下。九、测这两种JPEG容易踩哪些坑9.1 截断解码不等于浏览器实拍这是我这次花时间最多的一个坑。本来想在浏览器里限速截屏把两种文件加载到一半的样子拍下来。结果截到的画面要么是白的要么已经是整张图中间态一次都没有。我的猜测是截图那一刻浏览器还没把半截的图画上屏。也可能它等数据够多了才一次性画出来。具体是哪种我没查清楚。后来改成截断文件再解码这个结果稳定也能复现。可它说的是「解码器拿到这么多数据能画出什么」浏览器什么时候把这个画面真正显示给读者是另一回事。PSNR 本身也有偏向。它对大片缺失特别敏感对整体偏软不那么敏感天然偏向渐进式。拿它比同一张图的两种排法可以拿它去说渐进式快了多少秒就过头了。9.2 比体积前先把优化选项对齐这个坑 3.2 节已经讲过这里再说一次是因为它会直接把结论带偏。拿没开 optimize 的基线式去跟渐进式比会高估省下来的量。我第一次比出来是 6.0%对齐以后是 4.6%。我见过说渐进式能省一成多的不知道是不是也这么比出来的。WebKit 导出的那份转完小了 10.9%里面也有 4.9% 是只开优化就能拿到的。我用 jpegtran -optimize 单独转了一遍才把它拆出来。比之前先用同一个编码器、同一个质量并且同样开着 optimize。只动 progressive 这一个开关比出来的数才是排法本身的账。9.3 解码耗时要多跑几轮毫秒级的数晃得很厉害40 张缩略图那组在同一台机器上用同一个脚本跑了两次。渐进式一次 19.6 毫秒一次 16.8 毫秒差了将近 3 毫秒。单张大图每次重跑也都会有零点几到一两毫秒的出入。我的做法是每组解码 7 次取中位数再把整组跑两遍看量级稳不稳。只跑一次就下「慢了几倍」的结论不太靠谱。还有一点要注意createImageBitmap 量的是浏览器把一个文件完整解成图片的时间。它既不包括下载也不包括画到屏幕上。拿它比两种文件谁解码快是合适的拿它当成页面上看到图片要多久就不对了。十、还有哪些没弄清楚这次留了好几个没弄明白的地方最想知道的是浏览器里渐进式到底怎么显示。截屏那条路我没走通下次想换成录屏再试一次。WebKit 导出的 JPG 同样填 0.85 却比另外两个大了七成多。我只看出它跟 sips 出的是同一个文件它的质量刻度怎么换算我没查。libjpeg-turbo 默认那 10 个扫描段是怎么分的我也只猜了个大概。手机上渐进式要多花多少解码时间这篇完全没测。我只能说电脑上不要紧。两张样本的小图都在四千多到七千字节之间开始变大换一张内容复杂或简单得多的图会不会挪位置也还不知道。手上有一批首屏大图的话可以先拿第二章的脚本扫一遍文件头看看里面有多少是基线式。再用第七章那个脚本转一份出来对比一下体积然后决定换不换。参考资料ITU-T T.81JPEG 标准中关于顺序编码与渐进编码、SOF0 / SOF2 标记的说明libjpeg-turbo 文档中 cjpeg、djpeg、jpegtran 的用法说明-progressive、-optimize、-copy 参数Pillow 文档中 JPEG 保存参数 quality、optimize、progressive 的说明HTML 标准中 canvas 的 toBlob 方法说明样本来源Wikimedia Commons 上的一张风景照缩放后使用稿定设计的「2026年国庆节放假通知」模板图
返回列表