真正的问题不在体积,在第二次打开的时候
真正的问题不在体积,在第二次打开的时候
本文数据来自本机实测,复现脚本与原始输出均可提供

这个结论我一开始不信。

我手头有件事要做:把图片全用 base64 塞进 HTML,做成一个可以整体带走、粘进编辑器不掉图的文件。塞之前我想先知道代价,就顺手测了一遍。

第一版数字很难看。

120 张照片,明文大了 33.33%。

然后我开了 gzip,按能比的口径重算一次。

差距塌到 0.31%。

两个数差了 100 倍。

中间差的不是运气,是没人愿意多算的那一步。

测试怎么搭的

样本是 120 张真实照片,来自本机正在用的配图目录,不是找来的测试图。原图合计 17,902,030 字节。

造两份 HTML。图片数量、位置、宽高、图注完全一样,唯一区别是 src:

外链版写 src=「/uploads/kepu/xxx.jpg」。

内嵌版写 src=「data:image/jpeg;base64,....」。

然后算一件事:访问这个页面,服务器一共要吐多少字节。

外链版 = gzip(HTML) + 120 张原图。

内嵌版 = gzip(整页 HTML,图都在里面)。

只比 HTML 明文是耍流氓,因为外链版的图片根本不在 HTML 里,那个数没法当成本。

33.33% 是真的

两次重跑 expD.py 的原始输出,未改动
两次重跑 expD.py 的原始输出,未改动

base64 的膨胀率精确等于 4/3,也就是 33.33%。这是编码本身决定的,没有优化空间。

17,902,030 字节的原图,base64 后 23,869,528 字节。一个字节都不差,正好 4/3。

所以那个流传的说法不是谣言。

它只是没算完。

开 gzip 之后

同一个页面,两种放法的实际传输量
同一个页面,两种放法的实际传输量

外链版总共 17,903,266 字节。

内嵌版 17,959,257 字节。

多 55,991 字节,0.31%。

看起来像作弊,其实不是。我核对了一下原因:

JPEG 本身已经压过,gzip 对它只有 0.57% 的收益(17,902,030 变 17,799,730)。

base64 不一样,它能压到 75.19%(23,869,528 变 17,947,334),正好把那 33% 还回来。

至于为什么 base64 这么能压,我的推测是它的字符集只有 64 个符号,每 3 字节映射到 4 字符时高位模式很规整,zlib 容易吃。这条我没验证过,只是 75.19% 太接近 3/4,看着像巧合。

规模越小,内嵌越亏一点

我按 1 张、5 张、20 张、120 张分别算了一遍。

1 张图时内嵌多 0.79%。

5 张多 0.82%。

20 张多 0.71%。

120 张只多 0.31%。

方向很一致:块越大,gzip 压得越好,内嵌越不吃亏。

反过来说,「页面里就一两张小图」这个场景,内嵌相对更亏,虽然绝对值都不大。

真正的账在第二次

二次访问与首屏,这才是分水岭
二次访问与首屏,这才是分水岭

这条跟 gzip 一点关系都没有。

外链的图片能被浏览器缓存。

第二次打开走 304,一张图都不用重下,20 张图加起来 6,300 字节。

内嵌的图在 HTML 里。

HTML 一重下,图跟着重下,3,395,655 字节。

539 倍。

首屏也是同一个道理:内嵌必须把整页收完,浏览器才解析到 <img>。外链可以一边收 HTML 一边并行拉图。

这里要标清楚:首屏那两个数(4G 0.08s 对 2.16s,弱网 0.87s 对 22.11s)是我按带宽假设算出来的,不是拿浏览器实测的。 假设 4G 1.5MB/s、弱网 150KB/s、单线程。

真实浏览器会并行下载、会流式解析,内嵌的实际表现应该比 22.11s 好看一些。我留着这个数,是因为方向没错,绝对值别当真。

还有一个代价:gzip 是服务端的活。整页 23MB 的 HTML,gzip-9 要 1,235 毫秒 CPU。120 张图时才这样,20 张是 230 毫秒,1 张是 10 毫秒。

那我给你做的那个 653KB 文件白做了吗

不算白做,但理由跟上面的账无关。

那个文件的用法是:本地双击打开,Ctrl+A,Ctrl+C,粘到编辑器里。

全程不走网络。

0.31% 和 539 倍,都是网络传输的口径,在这个场景里不成立。

所以同一张图要不要塞进 HTML,取决于它要去哪。

上网络,就让浏览器缓存它;进剪贴板,就把它一起带上。

我没测的部分

brotli 本机没装,跑不了。所以上面全部是 gzip-9 的结果。brotli 对文本通常比 gzip 好 15% 到 20%,但对 base64 效果如何,我没有数据【本人未实测】。

HTTP/2、CDN 的图片转码、真实浏览器的首屏耗时,这几个都没测【暂无相关资料】。

还有一个我没想清楚的地方:如果页面只被打开一次,内嵌其实更优,因为省了建立连接的往返。但「只被打开一次」这件事,事前没法知道。

你的项目里图片现在怎么放?有没有为了省请求踩过坑,或者反过来,被缓存坑过?

要那张 120 张图的完整数据表和复现脚本的话,说一声,我整理出来。