CSS覆盖率实测:1149个类名里这一页只用上200个

CSS覆盖率实测:1149个类名里这一页只用上200个
张文保 更新 23 分钟阅读 4,329 阅读
本文目录
  1. 一份样式表里有多少条规则,这一页又用得上几条?
  2. 这个数字会不会是我自己量错的?
  3. 多抓几个页面,结论会不会翻盘?
  4. 那些用不上的规则,到底是谁写的?
  5. 为什么不同公司的站,样式表里会有同一批名字?
  6. 换个平台,这笔账差多少?
  7. 缓存设了一年,是不是就等于不用管了?
  8. 这件事到底影响不影响搜索排名?
  9. 该动手改什么,又有哪些地方不该碰?
  10. 常见问题解答
  11. 命中率17.8%是不是意味着可以删掉八成CSS?
  12. 为什么不用Chrome的Coverage面板来测?
  13. 样式表大一点到底要紧不要紧?
  14. 同样是电商,为什么不同平台的差距这么大?
  15. 有一批站为什么被整体剔出了样本?
  16. 缓存设成一年,是不是就不用管体积了?
  17. 怎么判断我站上的样式表是构建产物还是手写的?
  18. 权威参考资料
摘要:把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.7KB245KB
主样式表解压后字节269KB1.54MB
class定义条数11496007
规则块条数322025055
@media查询次数1422489
CSS自定义属性99362
!important出现次数2032745

这张表里有两个数字值得单拎出来说。一个是!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-cardsite-header__inner:59221条,占78.6%
  • 工具类,比如mt-4text-centerflex-wrap:14229条,占18.9%
  • 状态与交互类,比如is-openjs-active:1930条,占2.6%

状态类天生就不会出现在初始HTML里,它们要等用户点一下才被加上,这部分算“没命中”确实冤枉。但它们只占2.6%。真正的大头是组件类,占了近八成——这些名字对应的是实实在在的界面模块,只是那些模块不在这个页面上。

所以话得说到位:没命中不等于可以删。真正能安全删掉的比例,一定低于这个命中率的补数。但即便把状态类和工具类全部当成“用得上”放行,剩下那部分也仍然是这份文件里最厚的一层。

那些用不上的规则,到底是谁写的?

这才是我折腾这一圈真正想问的。把262份成功抓到的样式表按地址形态归类,答案挺清楚:

来源形态份数占比
Next.js构建产物(/_next/static/)8833.6%
自有域、看不出构建痕迹6324.0%
其它带内容哈希的构建产物5219.8%
店铺主题目录(/cdn/shop/t/)249.2%
Hydrogen构建产物(/oxygen-v2/)155.7%
第三方域145.3%
平台自带(/cdn/shopifycloud/)62.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-centeroverflow-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定义中位
Magento230.3%229KB31.9KB1161
Next.js1121.2%146KB24.6KB776
Shopify1318.7%356KB66.0KB1576
认不出平台1211.1%160KB25.8KB1082
Salesforce88.8%728KB95.7KB3201

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.csstheme.css这种,通常是主题或手写文件。这个区分很重要:构建产物能靠调整构建配置改善,主题自带的那份改起来风险大得多。

权威参考资料

分享到
标签
版权声明

本文标题:《CSS覆盖率实测:1149个类名里这一页只用上200个》

本文链接:https://zhangwenbao.com/css-coverage-unused-rules-audit.html

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

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