12k
All articles

使用 Project Wallace 审计样式表

使用 Project Wallace 进行 CSS 审计,可将唯一颜色、字号、特异性、重复和文件大小转化为具体修复。

OpenReplay Team
OpenReplay Team
使用 Project Wallace 审计样式表

用 Project Wallace 做一次 CSS 审计,意味着把样式表粘贴进在线分析器,然后读懂五个数字:唯一颜色数、唯一字号数、最大选择器优先级(以及旁边的 id 与 !important 计数)、声明总数与唯一声明数之间的差距,以及未压缩文件体积与 gzip 文件体积的对比。每一个数字都指向一项不同的改动。

如果你的样式表已经有三年历史,你多半已经怀疑它出现了漂移。你缺的是一个能写进工单的数字。“CSS 感觉很乱”不会被排上优先级;“设计规范只定义了两种灰,我们却上线了六种”才会。

本文将一份小型示例样式表送进分析器,逐项解读输出指标,并把每一个发现与它应当触发的具体改动配对。本文讲的是诊断。后续文章《如何在现代 Web 项目中组织 CSS》讲的是治疗。

核心要点

  • 浏览器会丢弃它无法解析或无法识别的 CSS 并继续渲染,因此样式表可以不断累积错误,却从不导致构建失败。
  • 当你粘贴或上传 CSS 时,Project Wallace 会在你自己的设备上通过 WebWorker 执行分析,样式表永远不会离开浏览器。
  • 已上线的唯一颜色数与设计规范所定义的调色板之间的差距,就是设计漂移的可量化形式,而修复手段是把这些值收敛为 token,而不是删除规则。
  • 出现在样式表中段的优先级尖峰,其代价高于位于末尾的高优先级最大值,因为后续每一次覆盖都必须爬升到与之匹配的高度。
  • 空规则是唯一一项永远不需要做视觉回归检查的审计修复;重复声明在移除前则需要判断。

为什么 CSS 审计能发现构建流程发现不了的问题?

浏览器遇到无法解析或无法识别的 CSS 声明时,会丢弃该声明并继续渲染页面,这正是样式表可以累积多年错误却从未导致任何一次构建失败的原因。这种恢复行为是规范规定的。根据 CSS Syntax Module Level 3 的错误处理规则,未构建完整的声明会被丢弃,解析器越过下一个分号,然后从那里继续正常解析。

其后果是,CSS 的损伤从来不表现为失败,而表现为漂移:第四种灰与第三种只差两个点、17px 的标题夹在 16px18px 两级之间、迫于工期加上的一个 id 选择器,随后又加上一个 !important 来压过它。这些都不会弄坏任何东西。但它们都会让下一次改动更难。

如何运行 Project Wallace 分析器?

Project Wallace CSS 分析器支持三种输入方式:网站 URL、上传文件,或直接粘贴 CSS。当你粘贴或上传 CSS 时,处理过程在本地完成:由你自己设备上的 WebWorker 执行分析,你粘贴的内容不会被发送到任何地方。这一设计可追溯到 2021 年的分析器重写。URL 模式则必然要先通过网络抓取目标站点,再进行分析。

输入页面上有一个 “Prettify CSS?” 开关,旁边有一条提示,警告该选项会略微改变数值。选定一种状态,并在所有需要相互比较的运行中保持不变。

下面是示例样式表。三位贡献者、两年时间、一个 header 组件和一个 card 组件:

/* header.css — three contributors, two years */
#site-header {
  background: #f5f5f5;
  color: #333333;
  font-size: 16px;
}

#site-header .nav-link {
  color: #343434;
  font-size: 15px;
  padding: 8px 12px;
}

.nav-link:hover {
  color: #222222 !important;
}

.card {
  background: #f4f4f4;
  color: #333333;
  font-size: 1rem;
  padding: 16px;
}

.card .card-title {
  font-size: 18px;
  color: #333333;
}

.card--featured .card-title {
  font-size: 17px;
  font-weight: 700 !important;
}

.legacy-banner {
}

.footer {
  background: #f5f5f5;
  color: #444444;
  font-size: 14px;
}

返回的是一个结果页面,其分组与指标文档中的类别一致:Stylesheet、Atrules、Rules、Selectors、Declarations、Properties 和 Values。指标总数远超一百项。下面这五项,是能真正转化为一次提交的那几项。

唯一颜色数和字号数说明了什么?

颜色总数与唯一颜色数之间的差距,说明每种颜色被复用的频率;而唯一颜色数与你的设计系统所定义的调色板之间的差距,说明代码偏离设计有多远。分析器会同时统计这两者,并给出它找到的全部唯一颜色列表;字号也是同样的处理方式。

手工读一遍,这份示例包含六个不同的十六进制字符串:用作表面色的 #f5f5f5#f4f4f4,以及用作文字色的 #333333#343434#222222#444444。针对这个组件的设计系统,几乎可以肯定原本只打算用一种表面色和两种文字色。字号阶梯更糟:16px15px1rem18px17px14px,字面上是六个值,而 16px1rem 通常本来就会解析为相同的像素大小。

修复手段是收敛,而不是删除。收敛这些几乎相同的灰色,不意味着删掉规则,而是把每个字面量替换成最接近的 token,让规则继续发挥它原本的作用:

:root {
  --color-surface: #f5f5f5;
  --color-text: #333333;
  --color-text-muted: #444444;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
}

.card {
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-base);
  padding: 16px;
}

三个颜色 token 取代六个字面量;三个尺寸 token 取代六个。Project Wallace 另有一个独立的 Design Tokens 工具,可以从现有 CSS 中提取候选颜色和字号,对于真实的样式表来说,这比手工通读列表要快得多。

优先级:尖峰比最大值更重要

位于样式表末尾的高优先级选择器只是一个局部问题,但位于中段的高优先级选择器,会迫使之后每一条需要覆盖它的规则都爬升到同一高度,而这种爬升正是 id 选择器和 !important 标记不断增殖的原因。Harry Roberts 的优先级图表以可视化方式表达了同样的论点:趋势线应当朝末尾平缓上升,任何尖峰都是由其后的一切来买单的成本。

分析器通过最大选择器优先级、达到最大优先级的选择器总数、优先级最高的选择器列表,以三段式的 id、class、type 值形式报告优先级,同时还给出 id 选择器总数、!important 声明总数以及 !important 声明占比。

这份示例以微缩形式展示了这一机制。#site-header .nav-link 位于 (1,1,0)。后面的 .nav-link:hover 处于 (0,2,0),压不过它,于是有贡献者搬出了 !important。一个 id 选择器催生了一个 !important 标记;而 .card--featured .card-title 上的第二个标记,压过的是一条根本没有设置 font-weight 的规则。

id 选择器计数和 !important 计数,是团队可以一次一个提交地减少、并在每次改动后重新测量的两个优先级数字。在示例中,把标记从 id="site-header" 改成 class="site-header",会把两条 header 规则压平到 (0,1,0) 和 (0,2,0),两个 !important 标记也随之变得多余。

重复声明与空规则

空规则会贡献字节数和一次选择器匹配,却不改变用户看到的任何东西,因此删除它是唯一一项永远不需要做视觉回归检查的审计修复。重复声明则不同:同一意图被写了两遍是一种坏味道,但删错副本会改变层叠结果。

分析器直接统计空规则总数.legacy-banner {} 就是示例中唯一的一个,它该被删掉。分析器没有独立的重复声明指标。请把声明总数与唯一声明总数对照阅读,把两者的差值当作重复数,同时要注意文档目前仍未明确空白或格式差异是否会让两条声明被视为不同。

color: #333333 在示例的三条规则中出现。正确的做法不是删掉其中两份,而是让这三处都走 var(--color-text),这样重复就从一次巧合变成了一个显式可见的共享决策。

为什么 gzip 文件体积会掩盖臃肿的样式表?

gzip 对重复内容压缩效率很高,因此一份充满重复声明的样式表,可能给出一个好看的压缩体积,而它的未压缩体积——也就是浏览器实际要解析的字节数——却在持续增长。分析器会把未压缩文件体积、gzip 文件体积和 gzip 压缩比一并报告出来。

压缩比上升就是那个信号:它意味着样式表变得更重复了,而不是变小了。真正影响未压缩体积的是规则数和声明数,这也是为什么上文的收敛工作会顺带把它降下来。文件体积是在其他修复完成之后用来复核的诊断指标,而不是一个可以单独去优化的目标。

跨版本跟踪一份样式表

要跨版本跟踪一份样式表,请把每个版本的原始 CSS 与其分析结果保存在一起,并在每次重新分析时使用相同的 Prettify 状态。CSS Diff 查看器会在浏览器中格式化两份粘贴进去的样式表并逐行比对,从而显示唯一颜色数或 id 选择器数在哪里发生了变化。

结论

当每个数字都能映射到一项改动时,样式表审计才对得起它花掉的时间:唯一颜色数和字号数映射到 token,id 选择器和 !important 标记映射到更扁平的优先级,空规则映射到删除,重复项映射到共享声明,文件体积则用来复核其余改动是否奏效。把你线上最大的那份样式表粘贴进分析器,记下这五个数字,然后为每个数字开一个 pull request。

常见问题

我能在命令行或 CI 中运行 Project Wallace 分析器吗?

可以。wallace-cli 这个 npm 包会在终端中运行同一套分析器:用 npm install wallace-cli 安装,然后运行 wallace path/to/styles.css,或者通过 stdin 把 CSS 管道输入进去;在 CI 脚本中可加上 --json 标志以获得机器可读的输出。CLI 的 4.x 版本要求 Node 20.12 或更高版本。若需以编程方式使用,可从 @projectwallace/css-analyzer 导入 analyze 函数,这是一个仅支持 ESM 的包,可同时在 Node 和浏览器中运行。

我应该分析 Sass 或 Tailwind 源文件,还是编译后的 CSS?

应分析用户实际收到的编译后 CSS,而不是 Sass、Less 或 Tailwind 源文件。变量、mixin 和 @extend 会在构建时展开,因此源文件会误报唯一颜色数、选择器数和文件体积。Project Wallace 自家的 Stylelint 插件在指向何处这一点上也是同样的说法:把它对准你实际交付的 bundle,因为唯一值的计数以及基于这些计数构建的各种比率,描述的是交付出去的那个文件,而非源文件。URL 模式本身抓取的就是站点对外提供的构建后样式表。

Project Wallace CSS Analyzer 和它的 CSS Code Quality 工具有什么区别?

CSS Analyzer 报告的是原始指标;CSS Code Quality 工具则接收这些输出,在其之上运行自己的检查,并把结果归纳为三项百分制评分:Performance、Maintainability 和 Complexity。当你需要把某个数字追溯到某条具体规则时用分析器;当你想要一份带有明确判断、可以分享给团队的摘要时,用 Code Quality。两者都支持 URL、上传文件或粘贴 CSS。

审计之后,我该如何防止唯一颜色数或优先级出现回退?

在你的 Stylelint 配置中加入 @projectwallace/stylelint-plugin。它基于同一套分析引擎提供了 60 多条规则,其中包括 projectwallace/max-unique-colors;它的 holistic 预设会从整体上评判文件(总数、平均值、比率、唯一性),而不是逐个节点地检查。用 recommended 预设,只需一行 extends 就能跑起来。在 CI 中针对构建后的 CSS bundle 运行它,这样一个新增第七种灰的 pull request 会在合并前就失败。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.