学完这篇你能分清哪些 CSS 写法真的会拖慢样式计算和渲染、哪些只是被误传的"性能迷信",并拿到一套可直接照做的改写和测量流程。
第一步:先搞懂浏览器是怎么匹配选择器的
浏览器匹配 CSS 规则时是从右往左看的,最右边那一段叫"关键选择器"(key selector)。
比如 .header .nav ul li a:
- 浏览器先找出页面上所有
a
- 再逐个往上查:父级是
li 吗?再往上是 ul 吗?……
所以真正决定成本的是:关键选择器的匹配基数 × 需要向上回溯的层数。一条 .btn 比 .header .nav ul li a 快,不是因为"短",而是因为要检查的节点少、回溯浅。
第二步:这几类写法确实会拖慢渲染
- 超长后代选择器:
#app .wrap .list ul li.item a.link span。关键选择器是 span,页面上所有 span 都要被拉出来尝试回溯,命中率还低。
- 以标签名做关键选择器 + 深层约束:
.list li 里的 li 会被全页扫描;如果写成 div div div p 这种结构依赖型写法,成本更高。
* 参与的多层组合:单独一个 * { margin: 0 } 其实很便宜;但 * * { ... }、.x * .y 这种会出现组合爆炸,DOM 一大就明显。
- 带通配或正则语义的属性选择器:
[class^="icon-"]、[href*=".pdf"]、[data-x$="end"]。它们不能走类名索引,只能逐元素比对字符串。
:has()、深层 :not()、:nth-child(2n) 夹在长链里:这类"关系型/计算型"伪类在大量节点上使用时,会让样式失效判断(style invalidation)变复杂——改一个 class,浏览器要重算的范围更大。
注意:不要把"选择器写得好看"当成性能目标。只有页面 DOM 规模大(数千节点以上)或频繁做增删改时,上面这些写法才会在 Performance 面板里显出几十毫秒的 Recalculate Style。小站点纠结这个,属于过早优化。
第三步:等价改写的具体做法
#app .list ul li a.title → 给元素加类,直接写 .post-title
.list li:nth-child(odd) a → 给行加 .list-row--odd
.card .body .meta .author span → .card-author(扁平命名,BEM 那套)
.nav > ul > li > a → .nav-link
[class^="icon-"] → 统一加一个 .icon 类,用 .icon-xxx
两条原则:关键选择器只用 class/id;层级控制在 2~3 层以内,能用子选择器 > 就别用后代空格。
另外可以用 :is() / :where() 合并重复前缀,比如 :is(.a, .b) .title,减少规则条数(:where() 权重为 0,用来重置很方便)。
第四步:怎么测出到底有没有变慢
- Chrome DevTools → Performance 面板,录制一次页面加载或一次交互,看
Recalculate Style / Layout 的耗时。
- 打开 Selector Stats:DevTools 设置 → Experiments 里启用后,Performance 录制时勾选,能直接看到每条选择器的匹配次数和耗时,按耗时排序就能找出罪魁祸首。
- Coverage 面板(Ctrl+Shift+P 搜 "Coverage")看未使用 CSS 占比。
- 控制台里用
performance.now() 包一段 document.querySelectorAll('.your-selector') 跑 1000 次,对比两种写法。
第五步:比选择器更该先解决的问题
按收益排序,往往是这些先动手:
- CSS 体积和阻塞:
style.css 加载完才渲染首屏,砍掉未使用 CSS 比抠选择器收益大得多。
- 别用
@import:它会串行加载,把多个文件合并成一个 <link>。
- 动画属性:对
top/left/width/height 做动画会持续触发布局,改成 transform / opacity。
- 减少无谓的
class 切换:样式失效范围越小,重算越快。
注意:will-change、contain 这类提示属性不要到处加,滥用反而会吃内存、拖慢合成。
小结
- 匹配从右往左,成本取决于关键选择器的匹配基数和回溯深度。
- 要改的是:超长后代链、标签名关键选择器、
* *、通配/正则属性选择器、大量 :has()/计算型伪类。
- 改写方向:关键选择器用 class、层级压到 2~3 层、必要时用
:is()/:where() 合并。
- 用 Performance 的 Selector Stats 和 Coverage 验证,别凭感觉优化。
- 优先级上,CSS 体积、阻塞加载、动画属性通常比选择器写法更值得先处理。