CSS覆盖率实测:1149个类名里这一页只用上200个
本文目录
- 一份样式表里有多少条规则,这一页又用得上几条?
- 这个数字会不会是我自己量错的?
- 多抓几个页面,结论会不会翻盘?
- 那些用不上的规则,到底是谁写的?
- 为什么不同公司的站,样式表里会有同一批名字?
- 换个平台,这笔账差多少?
- 缓存设了一年,是不是就等于不用管了?
- 这件事到底影响不影响搜索排名?
- 该动手改什么,又有哪些地方不该碰?
- 常见问题解答
- 命中率17.8%是不是意味着可以删掉八成CSS?
- 为什么不用Chrome的Coverage面板来测?
- 样式表大一点到底要紧不要紧?
- 同样是电商,为什么不同平台的差距这么大?
- 有一批站为什么被整体剔出了样本?
- 缓存设成一年,是不是就不用管体积了?
- 怎么判断我站上的样式表是构建产物还是手写的?
- 权威参考资料
摘要:把131个英文大站的首页样式表逐份拉下来数了一遍,能算清楚的有46个。它们的主样式表中位数是1149条class定义、3220条规则块、解压后269KB,而这些定义里真正出现在页面上的中位数只有17.8%——75299条定义里,65510条在我抓的页面上一次都没露面。更有意思的是它们的来路:59.2%的样式表文件名带着一串内容哈希,是构建工具吐出来的产物;一半的站里能找到Tailwind的工具类痕迹,三分之一带着Swiper。也就是说,这份被每个访客下载的文件,既不是为这一页写的,多半也不是为这个站写的。
这件事的起因很土。我在给一个做户外装备的独立站看首屏,Network面板里那份样式表压缩后68KB,解压开来将近700KB。客户问了一句很难回答的话:这里面有多少是我这个页面用得着的?
我当时给不出数。Chrome的Coverage面板能告诉你这一次交互用到了多少字节,但它测的是“刚才这几秒”,换个人操作数字就变。我想知道的是另一个问题:这份文件里的规则,有多大比例跟这个页面压根没关系。
于是把手头那份131个英文大站的清单翻出来,一个个去数。
一份样式表里有多少条规则,这一页又用得上几条?
先把数法交代清楚,后面那些数字值几分钱,全看这一步立不立得住。
对每个站,我拉首页HTML,把里面所有rel="stylesheet"声明的地址抠出来,逐个下载。同时把首页和一个内页的HTML里所有class属性拆成单词,拼成一个集合。然后取该站class定义最多的那份样式表,看它定义的class名有多少个出现在那个集合里。
131个站里,首页能正常拿到的70个。其余的要么限流要么直接拒绝,这批站被拒的原因后面会专门讲一句。70个里面,主样式表能干净解析的49个,静态HTML里class名超过100个、这把尺子才算立得住的46个。
这46个站长这样。
| 指标 | 中位数 | 最大值 |
|---|---|---|
| 主样式表压缩后字节 | 36.7KB | 245KB |
| 主样式表解压后字节 | 269KB | 1.54MB |
| class定义条数 | 1149 | 6007 |
| 规则块条数 | 3220 | 25055 |
| @media查询次数 | 142 | 2489 |
| CSS自定义属性 | 99 | 362 |
| !important出现次数 | 203 | 2745 |
这张表里有两个数字值得单拎出来说。一个是!important的中位数203次,最多的那份用了2745次。层叠规则本来是有优先级体系的,一份样式表里出现两千多次强制覆盖,说明它经历过很多次“改不动就压上去”的叠加。另一个是@media中位数142次、最多2489次——同一套组件为不同屏幕宽度各写一份,是文件膨胀最常见的来源。
1149条class定义,页面上出现的中位数是17.8%。换算成条数,一份定义了1149个类名的样式表,我抓的那两个页面加起来只用到了大约200个。
把46个站的定义数加总是75299条,其中65510条在我抓到的页面上一次都没出现,占87.0%。这个总量口径比中位数更难看,因为几个巨型样式表把分子拉高了:weber.com的主样式表定义了6007个class,casetify.com那份解压后1.54MB。
分布也很偏。命中率低于10%的有16个站,低于25%的30个站,能超过一半的只有1个——arcteryx.com,56.5%。这个站的主样式表只定义了352个class,是全样本里最小的那几份之一。
这里面藏着一条规律,后面几节会反复碰到:样式表越大,用得上的比例越低。不是大文件里塞了更多废料这么简单,而是文件一旦大到某个程度,它服务的对象就已经不是某一个页面了。
顺带一个对照,首页HTML本身解压后中位数623KB、平均31个script标签。样式表在整个页面的字节里不算最大的一块,但它是渲染路径上绕不开的那一块,这个区别在关键渲染路径那篇里拆过:JS可以defer,CSS默认就是阻塞的。
这个数字会不会是我自己量错的?
会。而且我在这上面栽了三次,其中一次差点让整篇结论反过来。
第一次是提取class的正则。最直接的写法是在整份CSS上找.某某某,结果它把url(font.woff2)里的.woff2、把w3.org里的.org全当成了class名。这些假名字只会落进分母,不会落进分子,等于凭空压低命中率。
第二次是我去修它。想法是先把声明块{...}整段挖走,剩下的就是选择器。写完一跑,allbirds那份样式表的class定义从1084掉到225。数字掉得太好看了,我盯着看了半天才反应过来错在哪:@media是嵌套的,反复挖最内层的花括号,会把外层@media连同里面所有选择器一起挖掉。修尺子的那一刀,比原来的偏差危险得多。
正确的做法是每挖一层之前,先把这一层花括号外面的class收走,再往里挖。改完重跑,allbirds是1081——跟最原始的1084只差3个。
把49个站的两种口径摆在一起看,脏净比的中位数是1.00,只有2个站多算超过20%。也就是说我第一次担心的那个误差其实几乎不存在,我第二次动手修它才真正造成了灾难。这个顺序值得记一下:改测量方法之前,先用一个已知答案的样本验一遍新方法。
第三次栽在分子上。有一批站的首页HTML里几乎没有class属性——prose.com是0个,sostrenegrene.com是1个。不是它们的CSS没用上,是它们的HTML要等JavaScript跑完才有内容可套。这类站有12个,包括aliexpress、temu、zalando、rituals这几个。它们的命中率算出来是0点几,但那个0点几什么都不说明,只说明我的尺子量的是一张空表。这12个站被整体剔出了分母。
剩下的自基线倒是干净。同一份样式表隔了几十分钟再抓一次,49个站里49个解压后字节完全相同,42个连压缩后的字节都一模一样。有一个例外值得记:sulwhasoo.com那份样式表,第一轮拿到322382字节,几十分钟后再去要,同一个地址回了404——它们中间发过一次版,文件名里的哈希变了。这件事在讲缓存那节还会回来。
多抓几个页面,结论会不会翻盘?
这一节是留给抬杠的。我只看了首页和一个内页,凭什么说“用不上”?换成商品页、分类页、政策页,那些class是不是就都用上了?
手上的数据能直接回答一半。主轮里我同时记了两个数:只看首页的命中数,和首页加内页的命中数。
结果是只看首页命中率中位数12.0%,加上一个内页涨到17.8%,每个站的增量中位数只有1.0个百分点。有43个站能算这个增量,其中10个站加了一整个页面之后一个新class都没多认出来。多认出的class条数中位数是12条,最大的一个站也只有158条。
按这个斜率外推,就算再加五个页面,也补不上剩下那八成的缺口。当然外推是外推,所以我本来准备了第二轮实验:每个站抓五个不同类型的页面,画一条命中率随页面数变化的曲线。
这一轮没跑成。等我回头去抓的时候,前面已经成功过的站几乎全部开始回429。这是我这次最实在的一个教训:需要两轮取数的实验,第一轮就得把速率压到最保守,别指望第二轮还能补。限流不是按请求间隔算的,是按一段时间窗口里你总共敲了多少下,而且这个窗口比想象中长得多。
换个角度看这批class的构成,也能佐证同一件事。把能解析的那49个站、合计75380条定义按名字形态分类:
- 组件与结构类,比如
product-card、site-header__inner:59221条,占78.6% - 工具类,比如
mt-4、text-center、flex-wrap:14229条,占18.9% - 状态与交互类,比如
is-open、js-active:1930条,占2.6%
状态类天生就不会出现在初始HTML里,它们要等用户点一下才被加上,这部分算“没命中”确实冤枉。但它们只占2.6%。真正的大头是组件类,占了近八成——这些名字对应的是实实在在的界面模块,只是那些模块不在这个页面上。
所以话得说到位:没命中不等于可以删。真正能安全删掉的比例,一定低于这个命中率的补数。但即便把状态类和工具类全部当成“用得上”放行,剩下那部分也仍然是这份文件里最厚的一层。
那些用不上的规则,到底是谁写的?
这才是我折腾这一圈真正想问的。把262份成功抓到的样式表按地址形态归类,答案挺清楚:
| 来源形态 | 份数 | 占比 |
|---|---|---|
| Next.js构建产物(/_next/static/) | 88 | 33.6% |
| 自有域、看不出构建痕迹 | 63 | 24.0% |
| 其它带内容哈希的构建产物 | 52 | 19.8% |
| 店铺主题目录(/cdn/shop/t/) | 24 | 9.2% |
| Hydrogen构建产物(/oxygen-v2/) | 15 | 5.7% |
| 第三方域 | 14 | 5.3% |
| 平台自带(/cdn/shopifycloud/) | 6 | 2.3% |
带明确构建痕迹的合计155份,占59.2%。所谓构建痕迹,就是文件名里那串index-NSNGQyLX.css式的内容哈希——这种名字不是人起的,是打包工具按文件内容算出来的。
这条线索比它看起来重要。一份手写的样式表,作者知道自己在给哪几个页面写样式;而一份构建产物,是打包工具把整个项目里所有组件的样式收拢到一起的结果。它的服务对象不是某一个页面,是这个代码库里所有可能被渲染出来的页面。你打开的这一页只是其中之一。
店铺主题目录那9.2%是另一个故事。Shopify的主题目录结构在主题架构文档里写得很明白,一套主题要覆盖首页、集合页、商品页、购物车、博客、政策页所有模板,所以主题作者必须把所有模板的样式都放进去。买主题的人拿到的是一整套,不是自己那几页需要的那一小块。这一点在挑主题的时候值得当成硬指标看,选主题那篇里讲的几条筛选标准可以补上这一条。
顺着这个逻辑,前面sulwhasoo那个404也就说得通了:内容哈希意味着文件改一个字节,地址就换一个。缓存策略因此可以设得很激进,代价是每次发版所有人都得重下一份完整的。
为什么不同公司的站,样式表里会有同一批名字?
我原本猜这批站的样式表会长得很像。真去比对,第一个数字就把这个猜想打掉了:class定义超过150条、够得上比对的44个站两两配对,class集合的Jaccard相似度中位数只有0.003。也就是说随便挑两个站,它们的类名几乎完全不重叠。
但换个粒度看就变了。把每个class名出现在多少个站上排一排:有44个名字覆盖了四成以上的站,468个出现在两成半以上的站上。最普遍的那些是这样的——
active出现在33个站,disabled28个,container26个,sr-only25个,hidden25个,text-center24个,focus24个,flex-wrap21个,overflow-hidden19个,swiper-slide18个。
这些名字没有一个是某位设计师拍脑袋想出来的。sr-only是无障碍社区的通用约定,text-center和overflow-hidden是Tailwind的工具类命名,swiper-slide直接来自Swiper这个轮播库。按指纹数一遍:
- 能找到Tailwind工具类形态的:22个站,占50.0%
- 带Swiper的:15个站,34.1%
- 带Slick、Splide或Flickity的:13个站,29.5%
- 能看出Shopify Dawn系命名的:9个站,20.5%
- 带Bootstrap栅格的:8个站,18.2%
- 用CSS Modules哈希类名的:5个站,11.4%
整体不像,局部高度雷同,这两件事同时成立并不矛盾。各家的业务组件各写各的,所以整体Jaccard低;而底下那层脚手架是同几套开源方案,所以高频名字高度集中。你花钱雇人写的是上面那层,字节的大头往往在下面那层。
相似度榜首那几对更直白。anker.com和eufy.com的Jaccard是0.604,共有1265个class名;anker和soundcore是0.480,eufy和soundcore是0.463。这三个品牌属于同一家公司,主题显然是复制粘贴过去的。Magento那边,rab.equipment和vaude.com的相似度是0.381,两家八竿子打不着的公司,只是用了同一套电商内核。
换个平台,这笔账差多少?
把46个站按平台分组,差距比我预想的大。
| 平台 | 站数 | 命中率中位 | 解压后中位 | 压缩后中位 | class定义中位 |
|---|---|---|---|---|---|
| Magento | 2 | 30.3% | 229KB | 31.9KB | 1161 |
| Next.js | 11 | 21.2% | 146KB | 24.6KB | 776 |
| Shopify | 13 | 18.7% | 356KB | 66.0KB | 1576 |
| 认不出平台 | 12 | 11.1% | 160KB | 25.8KB | 1082 |
| Salesforce | 8 | 8.8% | 728KB | 95.7KB | 3201 |
Salesforce那8个站是两头都占的:样式表最大,用得上的比例最低。解压后中位数728KB,是Next.js那批的五倍;命中率8.8%,不到Next.js的一半。canyon.com定义了3390个class,页面上出现127个;weber.com定义了6007个,出现241个。
Next.js那批表现好,原因不神秘:它默认按路由拆包,每个页面只加载自己那部分样式。这不是谁做了正确决策的结果,是框架的默认行为。同样地,Salesforce那批表现差也不是谁偷懒,那套平台的模板体系就是把全站样式打成一份。
这正是我想说的那件事:命中率这个数字,你的技术选型在你写第一行代码之前就已经替你定了大半。后面能靠优化找补的空间,比选型时那一下小得多。这跟框架站渲染模式那篇讲的是同一件事的两面:框架的默认值不是中性的,它同时决定了性能基线和搜索表现的起点。
缓存设了一年,是不是就等于不用管了?
这是我最常听到的反驳,而且它有一半是对的。
262份样式表里,159份的Cache-Control写着一年或更久,占60.7%,其中大部分还带着immutable。最常见的三种写法分别出现了43次、36次和35次,内容都是public, max-age=31536000那一套。这意味着回头客确实不用重下。
但有三种情况这份缓存帮不上忙。
第一种是首次访问。对一个靠自然搜索拿新客的站来说,首次访问恰好是最该优化的那次。页面速度那篇里说过,Core Web Vitals的字段数据来自真实用户,新访客的那一次也在里面。
第二种是发版。内容哈希的好处是缓存可以设成永久,代价是文件改一个字节地址就换。前面sulwhasoo那个从200变404的例子就是现场证据。一个几百KB的整包样式表,改一行按钮颜色,全站访客都得重下一整份。拆包拆得越细,这个代价越小,而拆包这件事同样是构建工具在替你决定。
第三种是爬虫。Googlebot要渲染页面就得取CSS,它有自己的缓存策略但并不跟着你的Cache-Control走。这部分开销落在抓取预算上,跟条件请求那篇算的是同一本账;压缩类型那篇里那个“nginx出厂只压HTML”的坑,正好也发生在CSS这类资源上。要是CDN边缘的缓存键还配错了,回源频率会更难看,这块可以对着CDN边缘缓存那篇的清单核一遍。
这件事到底影响不影响搜索排名?
先把话说死:样式表的覆盖率不是排名因素,Google没有这么一个指标。谁要是拿这个数字去跟老板说“我们排名掉了是因为CSS浪费”,那是在给自己挖坑。
它影响的是三条真实存在的链路。
一是渲染。Google渲染你的页面时要下载CSS,页面越重,渲染排队越久。这对JS依赖重的站尤其明显,那12个静态HTML里没有class的站,本身就已经在这条链路上很吃亏了。
二是抓取预算。抓取预算不只花在HTML上,渲染需要的子资源同样计费。Googlebot那个2MB上限算的是单个文件,而一次渲染要取的文件不止一个,这两个约束叠在一起才是完整的画面。想知道自己站上这笔账有多大,最直接的办法是翻日志,日志分析那篇里的做法可以直接照搬。
三是LCP。首屏那张图或那段标题要等CSSOM构建完才能画出来,样式表的下载和解析时间是实打实压在LCP里的。CLS和字体加载那两篇里的现象,追到底往往也是同一份样式表。想把这几百KB挪出关键路径,手上能用的工具无非是资源优先级和渲染跳过这两类,但它们改变的是加载次序,改变不了总量。
还有一层容易被漏掉:这些数字在监控里未必看得见。跨域加载的样式表如果没配好时序响应头,浏览器给你的耗时就是一排零,这个盲区在时序数据那篇里量过。而我这批样本里有5.3%的样式表来自第三方域,正好落在这个盲区上。
顺带说一句保哥自己排错过的次序。有个做小家电的客户,首屏一直压不下去,当时的判断是图片太多太大,压了一轮图收效甚微;回头翻瀑布图才发现真正卡住的是那份440KB的主题样式表挂在head里同步阻塞。压图片那件事花了两周,把样式表挪走只用了半天。从那以后再看首屏,第一句话都是先问样式表多大。
这里还有个容易混淆的地方。这套账跟错误页字节那篇算的不是一本:那边算的是单个HTML响应有多重,这边算的是渲染这个HTML还要额外拖多少子资源。两笔都在抓取预算里,但优化手段完全不同,别混在一起谈。要量自己站上单份文件的体积,抓取体积那套工具更直接。
该动手改什么,又有哪些地方不该碰?
先说不该碰的,因为这部分更容易出事。
不要拿着覆盖率报告去删主题自带的CSS。我这套统计只看了两个页面的静态HTML,命中率里没算JS动态加的类、没算伪类和属性选择器、没算别的模板。照着这个数字删,删掉的很可能是购物车抽屉或者移动端菜单的样式,而这些东西在你测的那一刻恰好没渲染出来。改版掉流量那篇里那些翻车案例,有一部分就是这么来的。
能做的事按性价比排下来是这几件。
第一件,先测一遍再谈优化。Chrome DevTools的Coverage面板能给你这一次会话的字节级覆盖,我这套静态口径能给你一个跨页面的下限,两个数字互相印证着看。别拿单一数字下结论。
第二件,按模板拆包。这是收益最大的一步,也是最需要动构建配置的一步。Shopify主题可以把section级样式单独抽出来按需引入,Next.js和Vite默认就按路由分块,问题往往出在有人为了省事把一堆样式全塞进了全局入口。
第三件,把构建期裁剪配对。如果站上用了Tailwind,类名扫描规则值得读一遍——它靠扫源码里出现的字符串来决定保留哪些类,拼接出来的类名它认不出来,这是最常见的误删源头。
第四件,把关键样式内联、其余异步。web.dev那篇延后非关键CSS讲得比我细。要注意内联的那部分会进每一份HTML,跟着每次抓取重复传输,别内联过头。
第五件,也是我认为最该改的:把这个指标搬到选型阶段。上面那张平台对照表里,最大的差距不是谁优化得好,是谁一开始选了什么。等主题装完、代码写完再来做减法,能捡回来的比选型时那一下少得多。
最后留一个可以自己动手的口径。打开你自己的站,取那份最大的样式表,数一遍里面的class定义,再数一遍首页HTML里出现的class名,相除。如果你拿到的数字落在17%上下,你和这46个大站就在同一条水平线上;如果远低于这个数,值得回头看看是不是有整套模板的样式在跟着走。
这个判断跟看工具生成的样式片段是同一个思路——先搞清楚你手里这份东西是完整样式还是某个默认值之上的差集,再决定拿它做什么。同理,看到压缩率报告也别急着高兴,CSS压缩这件事省下的是空格和换行,省不掉那些本来就不该传过来的规则。
常见问题解答
命中率17.8%是不是意味着可以删掉八成CSS?
不是。这个口径只看了首页和一个内页的静态HTML,没有算JS运行后加上的类名、没有算伪类和属性选择器、更没有算站上其它模板。真正能安全删掉的比例远低于八成。这个数字的用途是横向对照和选型判断,不是删除清单。
为什么不用Chrome的Coverage面板来测?
用了,但它回答的是另一个问题。Coverage测的是“这一次会话里执行到的字节”,你多点几下按钮,数字就变;换个人操作,数字又变。它适合定位单页面的具体浪费,不适合跨站横向比较。我这套静态口径可复现、可批量跑,代价是它只是一个下限。
样式表大一点到底要紧不要紧?
要紧的不是文件大小本身,是它挡在渲染路径上。一份400KB的样式表如果被拆成按路由加载的几份,每份几十KB,体感完全不同。真正需要盯的是首屏之前必须下载完的那部分有多大,而不是全站CSS加起来有多少。
同样是电商,为什么不同平台的差距这么大?
本次样本里Salesforce那批命中率中位8.8%、解压后中位728KB,Next.js那批21.2%、146KB。差别来自模板的组织方式:一个倾向把全站样式打成一份统一交付,另一个默认按路由拆包。这不是哪个团队更用心的结果,是平台默认行为的差异,而这个默认值在你写第一行代码之前就已经定了。
有一批站为什么被整体剔出了样本?
它们的首页HTML里几乎没有class属性,内容要等JavaScript执行完才渲染出来。这种情况下我的分子是空的,算出来的命中率没有意义,所以整体剔除。顺带一说,这批站在别的技术审计里也经常单独成一类,因为不执行JS的抓取拿到的确实是一张空表。
缓存设成一年,是不是就不用管体积了?
回头客确实不用重下,本次样本里六成的样式表缓存期就是一年起。但有三种情况帮不上忙:靠自然搜索来的首次访问、每次发版换掉文件名哈希导致的全量重下、以及爬虫渲染时取子资源的那部分开销。前两种直接落在真实用户身上,第三种落在抓取预算上。
怎么判断我站上的样式表是构建产物还是手写的?
看文件名。带一串随机字母数字的,比如index-NSNGQyLX.css或者87b42a08.css,是打包工具按内容算出来的哈希,属于构建产物;名字是style.css、theme.css这种,通常是主题或手写文件。这个区分很重要:构建产物能靠调整构建配置改善,主题自带的那份改起来风险大得多。
权威参考资料
本文标题:《CSS覆盖率实测:1149个类名里这一页只用上200个》
本文链接:https://zhangwenbao.com/css-coverage-unused-rules-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0