网页体积实测:118个首页58%的字节,缓存寿命0秒

网页体积实测:118个首页58%的字节,缓存寿命0秒
张文保 更新 22 分钟阅读 3,474 阅读
本文目录
  1. 一份首页HTML里,有多少字节根本不会单独发请求?
  2. 这个比例会不会是压缩之前的假象?
  3. 这些字节能在浏览器里留多久?
  4. 同一个站里,为什么会给出两套截然不同的答案?
  5. 那这些字节里,有多少是真的每次都不一样?
  6. 把判据再收紧一层
  7. 两头的例子
  8. 内联换回来的是什么?这笔账得两头记
  9. 还有一项最不起眼,却铺得最开
  10. 被写进文件里的图片,是这件事的极端形态
  11. 把两次访问的账分开算
  12. 这笔账对搜索有多大影响?
  13. 自己站上怎么量一遍,哪些该外置哪些不该?
  14. 第一步:先量出内联占了多少
  15. 第二步:确认HTML和外部文件的缓存头
  16. 第三步:抓两次,看哪些块真的变了
  17. 第四步:按这个顺序动手
  18. 不该做的两件事
  19. 这一轮的口径限制,一次说清
  20. 常见问题解答
  21. 内联脚本和样式到底是不是坏做法?
  22. 传输的时候都压缩过了,58.5%这个数字是不是夸大了?
  23. 首页本来就不该被缓存,那内联的东西不能缓存不也很正常吗?
  24. 怎么快速判断哪些内联块可以外置?
  25. 把内联脚本挪到外部文件,一定会更快吗?
  26. data地址内嵌的小图标,是不是也该拆出来?
  27. 这些内联字节会不会影响Google抓取和排名?
  28. 131个国际品牌站的数据,中小独立站能直接套吗?
  29. 权威参考资料
摘要:118个海外品牌电商站的首页HTML,解压后合计106433920字节,其中62287198字节是内联进文档里的脚本、数据、样式和图片——占58.5%,每站中位56.8%,最高的一个站是98.5%。这些字节有一个共同点:它们不产生独立请求,所以缓存寿命只能等于HTML文档本身的缓存寿命。而同一批站里,HTML的缓存寿命中位数是0秒,外部样式表和外部脚本是31536000秒。46个能配对的站里,41个站给外面的文件签了一年,给自己文档里那几十万字节签的是一秒都不留。

上一篇量首页视频的时候,顺手记了一笔:那些视频文件的Cache-Control几乎清一色是一年。当时觉得理所当然——视频不会改,缓存久一点天经地义。

转头去量同一批站的HTML文档,数字反过来了:能拿到缓存头的57个站,中位数是0秒。

这本身也很合理,首页内容天天变,不该被缓存住。问题出在夹在中间的那一层:被写进HTML文档里的那些脚本、数据和样式,既不是首页内容,也拿不到外部文件的缓存待遇。它们跟着文档走,文档不留,它们就不留。这一轮就是把这层字节量了一遍。

一份首页HTML里,有多少字节根本不会单独发请求?

口径先说死。这里说的“内联字节”指五类东西:按MDN对script元素的说明,带src的脚本正文会被忽略,所以这里只算没有src<script>正文、<style>块的正文、以type标成JSON或模板的脚本块正文、元素上style属性的值、以及所有以data:开头的内嵌资源。它们的共同点不是“多余”,而是不产生独立的HTTP请求。RFC 9111对HTTP缓存的定义里,缓存条目是以请求为单位建立的,没有请求就没有条目——这句话是本文全部推论的起点。

118个首页跑下来:

成分合计字节有这一项的站数非零站的中位最大
内联脚本373848101161470774940600
内联JSON数据块1598475574102663732718
内联样式块68732309125713662100
元素上的style属性17561091101768892806
data:内嵌资源1651166314043603
结构化数据1231788488813447

六项加起来62287198字节,占118份HTML合计106433920字节的58.5%。按站看,中位56.8%,四分位落在29.9%和73.7%,最大98.5%,最小0。

burrow.com是绝对值最大的一个:5146884字节的HTML里,4940600字节是一段内联脚本,剩下不到21万字节才是页面本身。这个量级已经逼近空壳首页那一轮里最夸张的样本。kotn.com是比例最高的一个:1524288字节里1499870是内联的,98.4%,其中1491222字节是一个JSON数据块。

这个比例会不会是压缩之前的假象?

这是我预判会被第一时间反驳的地方,所以先自己拆一遍。上面那些数字都是解压后的。真实传输走的是gzip或者brotli,而脚本和JSON是高度可压缩的文本,压完之后占比理应大幅下降。

于是把116份能算的HTML各做了一次对照:整份用brotli压一遍得到一个数,把上述五类内联内容的正文全部挖空、只留标签外壳再压一遍得到第二个数,两个数的差就是内联部分在传输层的净代价。gzip也照做一遍。

口径每站中位占比均值最大116站合计
解压后54.6%52.0%98.5%61770529 / 106430179
gzip之后62.9%59.9%96.7%9915512 / 15089230
brotli之后64.9%60.7%97.7%7424720 / 11280538

压缩之后这个比例不降反升,从54.6%涨到64.9%。原因不难想:剥掉内联内容之后剩下的是HTML标签,标签的重复度极高、压缩率比脚本更好,所以分母缩得比分子还快。burrow.com那份传输时是402652字节,其中384854字节是那段内联脚本,95.6%。

所以“传输时是压缩的”这句话在这个题目上不成立。它反而让问题更严重一点。顺带说一句,压缩本身也有它自己的坑,服务器出厂只压哪几种类型那一轮量过一遍,很多站压的只有HTML这一种。

这些字节能在浏览器里留多久?

这是本轮的核心对照。同一批站,三种字节,三个数。

字节的类型拿到缓存头的站数max-age中位写成0秒的≥30天的最大
HTML文档(内联字节跟着它)57(另有61个站没写这个头)0秒46014400
外部样式表10831536000秒293315360000
外部脚本10231536000秒1868315360000

统计口径里把no-storeno-cache都记作0秒。按MDN对Cache-Control的说明,这两者语义并不相同——no-cache允许存但每次要回源验证,no-store连存都不许——但对“下次访问要不要重新传这几十万字节”这个问题,它们的结果是一样的:要。no-cache那一档理论上可以靠条件请求省下正文,可惜真能回出304的站只有三分之一,而且首页天天变,验证多半也验不过。

HTML这一行还有个细节:57个有缓存头的站里,最长的一个只有14400秒,也就是4小时;没有任何一个站给自己的首页文档超过30天。而外部样式表这一行有93个站超过30天,最长的给到315360000秒,十年。带哈希文件名的资源敢这么设,是因为改了内容就换地址,只是那些地址一年之后有一半会404,这是另一笔账。

同一个站里,为什么会给出两套截然不同的答案?

把HTML和外部脚本两个数都拿到的46个站配起来看:41个站外部脚本活得比HTML久,4个一样久,只有1个反过来。倍数的中位是2592001倍。

这个倍数大到没什么解读价值,因为分母是0。真正值得看的是几个具体的站——它们都不是小作坊,配置也都经过专人调过:

站点内联字节占HTMLHTML的max-age外部样式表外部脚本
anker.com149025077.6%03153600031536000
kotn.com149987098.4%03153600031536000
soundcore.com155893081.8%03153600031536000
ouraring.com123497289.1%03153600031536000
eufy.com163729970.5%03153600031536000
burrow.com494412596.1%90031536000没取到
govee.com300425691.7%131536000没取到

这不是配置写错了。给HTML设max-age=0是对的,给带哈希文件名的静态资源设一年也是对的,两条规则各自都符合web.dev那份HTTP缓存指南的建议。问题在于有一百多万字节被放在了规则一那边,而它们的性质更接近规则二

这类矛盾在响应头层面并不罕见,做Cache-Control这条指令到底写给谁那一轮时就撞见过一次:指令写着每次回源,CDN手上那份副本已经躺了9小时。区别在于那次是两个执行者理解不一致,这次是同一个执行者面对两类被混在一起的内容。

那这些字节里,有多少是真的每次都不一样?

为内联辩护最有力的一句话是:这些内容是动态的,每次都不同,外置了也缓存不住。这句话对不对,可以直接测。

做法是隔一段时间把同一批首页再抓一遍,把两次的内联块逐块算哈希再对比。之所以比“块”不比整份文档,是因为页面本身一直在抖——这件事上一次做对比实验时已经量过,不先立一条基线,任何“变了没有”的结论都站不住。

115个站的结果:合计59733261字节的内联内容里,两次逐字相同的有31064298字节,52.0%。按站看,中位数是80.9%不变;24个站两次完全一致;32个站低于一半;4个站是0。

把判据再收紧一层

块级哈希是个很粗的判据——一个块里改一个字符,整块就记作“变了”。所以又做了一次:在两次抓取块数完全相同的110个站里,看有多少块是“字节长度分毫不差、只是哈希不同”。

结果是中位100%、均值94.7%。也就是说所谓的“变了”,绝大多数是等长替换——一个时间戳、一个会话ID、一串随机数,长度不变、位置不变,只有那几个字符不同。

合起来,52.0%这个数字应该被理解成一个下界:至少一半的内联字节是逐字不变的,剩下那一半里又有九成以上只是被几个字符污染。真正每次都不一样的内容,比“动态”这个词让人以为的少得多。

两头的例子

完全不变、内联字节又多的一头:buckmason.com 616832字节分在66个块里,两次一模一样;babybjorn.com 466656字节分13块;traeger.com 451793字节分74块。这些内容住在外部文件里没有任何障碍。顺带一提,这三个站的内联块数分别是66、13、74,说明碎块多不等于内容动态,跟第三方拉进来的那一堆域名也没有必然关系。

另一头是govee.com,3000274字节的内联内容只有0.2%逐字不变;quince.com 2199487字节,0.5%;eufy.com 1635561字节,0.1%。这几个站内联的是带状态的商品数据,确实动态。但注意它们的块数两次都完全相同——变的是内容里的值,不是结构。结构和值分开外置,是这类站唯一可走的路,也是它们目前没走的路。

内联换回来的是什么?这笔账得两头记

写到这里必须给内联说句公道话,否则这篇就成了单方面控诉。

内联省掉的是一次请求往返。对首屏关键路径上的样式来说,这一省非常值钱——外置一份关键CSS意味着浏览器要多等一个往返才能开始渲染,关键渲染路径那一套打法的核心就是把首屏必需的那一小撮样式内联进去。按MDN对style元素的说明,这本来就是它的正当用途之一。

所以问题从来不是“能不能内联”,而是“内联多少”。样本里内联样式块的中位是25713字节——首屏关键样式通常在14KB上下,这个量级还算克制。真正失控的是另外两项:内联脚本中位147077字节,内联JSON数据块最大的一个3732718字节。这两样都不在关键渲染路径上,它们内联的理由通常不是性能,是打包工具的默认行为或者服务端渲染框架的数据注水。

顺带一个数量上的观察:每站内联脚本的条数中位是32条、最多257条;外部脚本中位21条、最多319条。也就是说多数站的内联脚本条数比外部脚本还多。这些碎块里有相当一部分是第三方标签自己插进来的初始化代码,而第三方脚本会自己变,你既控制不了它的内容,也没法给它单独设缓存。

还有一项最不起眼,却铺得最开

元素上的style属性,110个站都有,是六项里覆盖面最广的一项。它的中位只有1768字节,看着无关紧要,可分布拖着一条长尾:misen.com一个站892806字节,dreametech.com 13378字节,boohoo.com 38406字节。前者的成因很典型——模板给每一张商品卡片、每一个栅格单元都写了行内样式,卡片越多,这一项涨得越快。

行内样式还有一个额外代价:它没法被样式表里的规则复用,也享受不到任何压缩之外的优化。一条规则写在样式表里是一份,写在1000个元素上就是1000份。这跟前端库版本停在四年前那种历史欠账不同,它是每次渲染都在重新产生的欠账。

被写进文件里的图片,是这件事的极端形态

data:开头的内嵌资源把这个问题推到了头。按MDN对data URL的说明,它把文件内容直接编码进地址里,base64编码本身会让体积涨三分之一,而且它没有独立的缓存条目、没有独立的解码优先级。

规模上它不算大:118份HTML里578条、合计165116字符;主样式表里595条、合计276738字符。MIME分布是SVG 686条、GIF 154条、PNG 89条、JPEG 64条,还有14条把字体也编进去了。把光栅图编进文档这件事本身就不划算,图片压缩那一轮量过一个反例:135字节的图标经过一轮处理反而变成560字节。casetify.com一个站的主样式表里就有110条,171905字符。

真正有意思的是重复。浏览器不会替你把两段一模一样的data地址合并成一份——它们是两段字符串,不是两个资源。HTML里同内容重复的有47组、多占27503字符;主样式表里75组、多占158785字符。样式表那一栏尤其亏:那份文件本来能缓存一年,白白胖了15万字符。

这跟样式表里那些一次都没用上的规则是同一个方向上的浪费,只是那次浪费在“用不上”,这次浪费在“重复了”。

把两次访问的账分开算

做个粗略的算术能看清这件事的分量。取一个中位水平的站:HTML传输后大约96000字节,其中内联部分占64.9%,约62000字节;外部脚本和样式表假设合计500000字节,缓存一年。

首次访问,用户下载562000字节,内联部分占11%,无关紧要。第二次访问,外部那500000字节全部命中本地缓存,用户实际下载的只有那份HTML——96000字节,内联部分占其中的64.9%。从第二次访问开始,用户下载的每三个字节里就有两个是内联内容,而其中至少一半是上一次已经下过、逐字未变的东西。

这个算术对爬虫更不利,因为爬虫的复访频率比人高得多,而且它通常不带本地缓存。同一批站此前量过抓取体积的上限在哪,两件事放在一起看,内联内容吃掉的是同一份额度。

这笔账对搜索有多大影响?

分三层,影响强度递减,别混着说。

第一层最实在:复访成本。一个回头客再来一次,外部的脚本和样式表命中本地缓存,而这几十万字节的内联内容要整份重传。首屏渲染要等文档下完才能开始,这一层直接落在LCP上。要把它变成能追踪的数字,得先理清多层缓存和Core Web Vitals的关系

第二层是抓取。爬虫每抓一次页面就要传一次这些字节,页面越多、抓得越勤,总量越大。这跟页面体积与抓取截断那一轮讲的是同一个池子,也和连一个404页面都要66KB属于同一类开销。数据上要留个余地:58.5%这个比例本身不会直接影响排名,它影响的是速度和抓取效率,而这两项才跟排名有关。

第三层最弱,但值得提一句:机器读页面的时候,这些内联内容会把正文比例稀释得很难看。首页空壳那一轮量的是“这些字节里有没有字”,本轮量的是“这些字节明天还在不在”,两个问题问的是同一堆字节的两个属性。

自己站上怎么量一遍,哪些该外置哪些不该?

整套二十分钟,不需要任何付费工具。

第一步:先量出内联占了多少

浏览器里查看页面源代码,全选复制到编辑器,看总字符数;再搜<script<style,把没有src的那些块的长度加起来。占比超过一半就值得往下查。命令行更快:把首页存成文件,用一句正则把内联块挖空,比对前后大小。

第二步:确认HTML和外部文件的缓存头

开发者工具网络面板,点开文档那一行看响应头的Cache-Control,再点开任意一个JS或CSS看同一个头。如果这两个数字看着都很奇怪,先确认响应头本身有没有自相矛盾——资源提示那一轮里就有大量写了却从未生效的声明。两个数摆在一起,就是本文那张对照表在你站上的版本。

第三步:抓两次,看哪些块真的变了

隔十分钟抓两份HTML存成两个文件,做行级差异。只要一个块两次一模一样,它就没有理由待在一个不许缓存的文档里。这一步比前两步更重要,因为它直接给出可以外置的清单。

第四步:按这个顺序动手

  • 先动内联JSON数据块。它体积最大、最容易外置,改成一个带哈希文件名的接口或静态文件,缓存直接给一年。
  • 再动那些两次逐字相同的脚本块。它们多半是打包工具塞进来的运行时或者初始化代码,配置里通常有一个开关能让它们回到外部文件。
  • 内联样式块只砍首屏之外的那部分,首屏关键样式留着别动。
  • data地址里超过几KB的挑出来还原成独立文件,尤其是主样式表里重复出现的那些。
  • 元素上的style属性单独看,110个站都有,中位1768字节不算多,但misen.com一个站892806字节,那是模板在给每个元素写行内样式,跟srcset那一轮里模板批量生成的冗余属性是同一个来路。

不该做的两件事

不要为了降这个比例把首屏关键CSS外置,那是拿渲染时间换缓存,方向反了。也不要把内联脚本一股脑挪出去就完事——挪出去之后要确认新文件真的拿到了长缓存头,否则只是把一次传输换成了两次请求。保哥去年帮一个做小家电出海的团队做过一轮这种搬迁,脚本外置了,可打包配置里文件名没带哈希,运维只好把缓存设成5分钟,最后总请求数涨了、传输量没降。

这一轮的口径限制,一次说清

  • 只测首页。商品页和列表页的内联结构通常不一样,商品页的JSON数据块往往更大。
  • 只抓了两次,间隔是小时级。抓十次、隔一天再抓,“不变”的比例还会往下走一点。
  • 压缩对照用的是本地brotli的第5档和gzip的第6档,各家CDN实际用的档位不同,绝对值会有出入,但三种口径的相对关系不受影响。
  • HTML的缓存头有61个站压根没给,这61个站没有进那张对照表。没给这个头不等于能缓存,浏览器会用启发式规则自己猜,猜出来的通常也很短。
  • 外部脚本每站只取了一个样本,取的是HTML里第一个同域脚本。同一个站不同脚本的缓存策略可能不同。
  • 没有区分HTTP版本和CDN厂商,也没有测边缘缓存那一层,那是另一套问题,边缘缓存的分层与回源里单讲过。

最后交代一句方法上的连续性:这批数字全部来自静态抓取的HTML,没有让页面跑起来。JS渲染那一轮已经证明这条路会漏掉一部分内容,上一篇量首页视频就被这一点狠狠修正过一次。不过对内联字节这个题目,静态抓取恰好是对的口径——这些字节本来就是文档到达时就已经在那儿的,不需要等任何脚本跑起来。这也正是它们和视频字节最大的区别:视频的账单要等条件成熟才出现,内联的账单在文档送达的那一刻就已经付清了

常见问题解答

内联脚本和样式到底是不是坏做法?

不是。首屏关键样式内联是标准打法,能省掉一个渲染阻塞的往返。问题在量和种类:样本里内联样式块中位25713字节,量级正常;内联脚本中位147077字节、JSON数据块最大3732718字节,这两样不在关键路径上,内联的理由通常只是默认配置。

传输的时候都压缩过了,58.5%这个数字是不是夸大了?

反过来。用brotli把整份HTML和挖空内联内容后的HTML各压一遍,内联部分占传输体积的比例是每站中位64.9%,比解压后的54.6%还高。原因是剩下的HTML标签重复度更高、压得更狠,分母缩得比分子快。

首页本来就不该被缓存,那内联的东西不能缓存不也很正常吗?

首页不该被缓存是对的,但内联在里面的东西未必属于首页内容。实测两次抓取之间逐字不变的内联字节占52.0%,每站中位80.9%,24个站两次完全一致。这部分内容跟着一个不许缓存的文档走,是被牵连的,不是它自己的性质决定的。

怎么快速判断哪些内联块可以外置?

隔十分钟抓两份首页HTML做行级差异。两次一模一样的块直接进外置清单。如果块的字节长度没变、只有几个字符不同,那是时间戳或会话ID,把这几个值抽出来单独注入,其余部分照样可以外置。

把内联脚本挪到外部文件,一定会更快吗?

不一定。挪出去之后要确认三件事:新文件的文件名带内容哈希、缓存头给到长期、并且没有变成阻塞渲染的同步脚本。三条里少一条,可能只是把一次传输换成了两次请求,首次访问反而更慢。

data地址内嵌的小图标,是不是也该拆出来?

几百字节的SVG图标留着无所谓,它省下的请求比它多占的体积值钱。该拆的是两类:单条超过几KB的,以及同一份文件里重复出现的。实测主样式表里有75组重复,多占158785字符,而样式表本来能缓存一年,这部分亏得最冤。

这些内联字节会不会影响Google抓取和排名?

它不是排名因素,影响的是速度和抓取效率。爬虫每抓一次就要重传这些字节,页面多、抓得勤的站总量可观;用户侧则落在文档下载时间和LCP上。把它当成性能和抓取预算问题处理,别当成排名信号。

131个国际品牌站的数据,中小独立站能直接套吗?

方向能套,数值不能。样本偏大型品牌,技术栈以Shopify和Next.js为主,服务端渲染注水的JSON数据块特别大。小站的内联量通常小得多,但比例未必更低——用的是同一批模板和同一批第三方标签。还是得按上面四步在自己站上跑一遍。

权威参考资料

分享到
标签
版权声明

本文标题:《网页体积实测:118个首页58%的字节,缓存寿命0秒》

本文链接:https://zhangwenbao.com/html-inline-bytes-cache-lifetime-audit.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交