第六篇 CSS 进阶:深入浏览器与工程化
目标:从"能写页面"升级到"理解浏览器、写得稳、跑得快、好维护"。
看完这一篇,你应该能:
- 更深入地理解层叠、优先级、层叠上下文与 z-index;
- 知道哪些 CSS 写法性能更好,哪些可能拖慢渲染;
- 理解 Sass、PostCSS 等工具处在 CSS 流程的什么位置,以及它们各自解决什么问题。
第15章 层叠与优先级的真相
这一章解决的核心问题:
- 为什么同一个元素的颜色,在不同地方写,会"互相打架"?
!important、选择器特异性(优先级)、来源顺序之间到底谁说了算?- 元素层级(谁压在谁上面)和 z-index 的真实规则是什么?
你会发现,"调样式"时的很多迷之现象,其实都是层叠规则在发挥作用。
15.1 层叠(Cascade)与来源
15.1.1 样式从哪里来?
浏览器最终给一个元素计算样式时,要综合来自多方的规则:
-
用户代理样式(User Agent Style)
- 浏览器内置的默认样式
- 例如:
h1默认字号比p大,a标签默认蓝色带下划线 - 每个浏览器都有自己的默认样式表
-
用户样式(User Style)
- 用户在浏览器设置中自定义的样式(现在较少使用)
- 例如:用户设置最小字号、强制使用某种字体
-
作者样式(Author Style) ⭐ 最重要
- 开发者编写的样式
- 来源:外部CSS文件、
<style>标签、style属性 - 这是我们日常工作的主要部分
大多数情况下,我们主要关心"作者样式"的层叠,但要知道:
- 默认样式不等于"没有样式",只是你没写而已
15.1.2 当多条规则命中时的决策顺序
对于同一个属性(比如 color),如果有多条规则都命中了同一个元素,浏览器会按大致顺序做决策:
- 重要性(Importance) :有
!important的优先 - 来源(Origin):一般作者样式 > 用户样式 > 浏览器默认(视情况而定)
- 特异性(Specificity):谁的选择器更"具体"
- 顺序(Order):写在后面的覆盖前面的
可以用一条「层叠决策线」来理解:
txt
层叠决策顺序(优先级从高到低):
1. !important 声明 → 最高优先级
2. 来源(Origin) → 作者样式 > 用户样式 > 浏览器默认
3. 特异性(权重) → ID > 类 > 标签
4. 书写顺序 → 后写的覆盖先写的
例如:
.text { color: blue; } /* 权重:10 */
#main .text { color: red; } /* 权重:110 */
.text { color: green !important; } /* !important 最高 */
最终生效:green(因为有!important)
第 2 章已经讲过特异性基础,这里会更结合实际场景和层叠上下文去看。
15.2 层叠上下文与 z-index 解密
在页面中,元素之间谁在上、谁在下,不完全由 DOM 顺序决定,还受到"层叠上下文(stacking context)"的影响。
15.2.1 什么是层叠上下文?
可以把层叠上下文想象为一个"局部三维空间":在这个空间里,元素可以通过 z-index 控制前后顺序;
不同层叠上下文之间,有自己的"整体层级"。
一个简单的比喻:
- 整个页面是一个大白板
- 某个卡片(有
position+z-index)可能自己形成一个小白板 - 小白板内部的前后关系,不会影响到其它小白板内部的前后关系
15.2.2 什么会创建层叠上下文?
常见会创建层叠上下文的情况:
css
/* 1. 根元素 */
html { } /* 页面根层叠上下文 */
/* 2. 定位元素 + z-index */
.modal {
position: relative; /* 或absolute/fixed/sticky */
z-index: 100; /* 非auto值创建层叠上下文 */
}
/* 3. 透明度小于1 */
.fade {
opacity: 0.9; /* 创建层叠上下文 */
}
/* 4. transform变换 */
.card {
transform: translateZ(0); /* 任何非none值都创建 */
}
/* 5. filter滤镜 */
.blur {
filter: blur(5px); /* 创建层叠上下文 */
}
/* 6. mix-blend-mode混合模式 */
.overlay {
mix-blend-mode: multiply; /* 创建层叠上下文 */
}
示意:
txt
页面根(层叠上下文 A)
┌─────────────────────────────┐
│ div#modal-root (z-index: 1000) ← 层叠上下文 B │
│ └─ modal 内部元素 │
│ │
│ div#page (z-index: auto) ← 层叠上下文 C │
│ └─ header / main / footer │
└─────────────────────────────┘
B 整体永远压在 C 之上;
C 内部再决定各自元素的前后顺序。
15.2.3 z-index 的比较只在"同一个层叠上下文"里有效
一个常见坑:
html
<div class="parent-a">
<div class="box-a">A</div>
</div>
<div class="parent-b">
<div class="box-b">B</div>
</div>
css
/* 层叠上下文的常见误区示例 */
/* 父容器A:创建层叠上下文,层级1 */
.parent-a {
position: relative; /* 定位元素 */
z-index: 1; /* 创建层叠上下文,整体层级为1 */
}
/* 父容器B:创建层叠上下文,层级2 */
.parent-b {
position: relative; /* 定位元素 */
z-index: 2; /* 创建层叠上下文,整体层级为2 */
}
/* 子元素A:虽然z-index很大,但受限于父容器 */
.box-a {
position: absolute; /* 绝对定位 */
z-index: 9999; /* 再大也只在parent-a内部生效 */
/* 整体仍然在parent-b下面! */
}
/* 子元素B:z-index小但父容器层级高 */
.box-b {
position: absolute;
z-index: 1; /* 虽然只有1,但parent-b整体在上 */
/* 所以box-b会覆盖box-a */
}
很多人会以为:box-a 的 z-index: 9999 一定压过 box-b,
但实际上:整个 parent-b(z=2)层叠上下文都压在 parent-a(z=1)之上。
可以用图示理解:
txt
层叠上下文树状结构:
页面根(层叠上下文0)
│
├─ parent-a (z-index: 1) ──────┐ 层级1:整体在下
│ └─ box-a (z-index: 9999) │ 9999只在parent-a内部有效
│ │ 无法突破父容器层级限制
│
└─ parent-b (z-index: 2) ──────┐ 层级2:整体在上
└─ box-b (z-index: 1) │ 虽然只有1,但父容器赢了
│ 所以box-b覆盖box-a
记住:子元素的z-index再大,也无法超越父容器的层叠上下文!
结论:不要指望用"极大值 z-index"解决所有层级问题,更重要的是理解层叠上下文结构,并在合适的容器上设置正确的 z-index。
15.3 浏览器渲染顺序(简化版)
理解浏览器如何渲染,有助于你写出更高效、可预期的 CSS。这里给出一个简化流程:
txt
浏览器渲染流程详解:
1. 解析 HTML → 构建 DOM 树
将HTML标签转换为树状结构的对象
2. 解析 CSS → 构建 CSSOM 树
将CSS规则转换为样式对象树
3. 合并 DOM + CSSOM → 渲染树(Render Tree)
只包含可见元素,display:none的不在其中
4. 布局(Layout/Reflow):计算位置和大小
确定每个元素的精确位置和尺寸
⚠️ 改变width/height/margin等会触发
5. 绘制(Paint):填充像素
填充颜色、边框、文字、阴影等视觉效果
⚠️ 改变color/background/box-shadow等会触发
6. 合成(Composite):图层合并
将多个图层合成最终图像显示到屏幕
✅ transform/opacity只触发这一步,性能最好
ASCII 示意:
txt
HTML CSS
↓ ↓
DOM CSSOM
\ /
Render Tree
↓
Layout
↓
Paint
↓
Composite
我们后面讲性能优化,会尽量避免那些会频繁触发布局和重绘的写法。
第15章从"谁的规则生效"到"谁在谁上面",把层叠与层级关系梳理了一遍。接下来第16章,我们会更关注"写得快不快、渲染累不累",深入 CSS 性能优化的一些实际建议。
第16章 CSS 性能优化
这一章解决的核心问题:
- 哪些 CSS 写法对性能影响大,应该尽量避免?
- 如何减少不必要的重排(reflow)和重绘(repaint)?
- 在复杂页面中,如何用 DevTools 粗略判断性能问题?
16.1 避免昂贵选择器
浏览器在应用 CSS 时,需要对每个元素判断"这个选择器是否命中"。大多数现代浏览器已经对这块做了大量优化,但极端复杂的选择器仍然会增加匹配成本。
16.1.1 选择器匹配的基本策略
浏览器通常是从选择器的"最右边那一段"开始匹配的:
css
/* 浏览器从右往左匹配选择器 */
.sidebar ul li a.active {
color: #2563eb;
}
/* ↑ 第一步:找所有 a.active
↑ 第二步:检查是否在 li 中
↑ 第三步:检查是否在 ul 中
↑ 第四步:检查是否在 .sidebar 中
*/
这里会先找所有满足 a.active 的元素,然后再回头检查它们是否处在 .sidebar ul li 的结构中。
css
/* ✖ 不推荐:过度复杂的选择器 */
body > main > section > div.container ul li a span.text {
/* 层级太深,匹配效率低 */
/* DOM结构一变就失效 */
}
/* ✖ 不推荐:脆弱的标签选择器 */
ul li a span {
/* 依赖HTML结构 */
/* 可能匹配太多不想要的元素 */
}
/* ✔ 推荐:简洁的类名选择器 */
.nav__link--active {
/* 直接命中,效率高 */
/* 不依赖DOM结构 */
color: #2563eb;
}
/* ✔ 推荐:适度的组合 */
.sidebar .nav__link {
/* 只有必要时才加父级限定 */
/* 保持简洁 */
}
16.1.2 避免使用万能选择器组合大范围匹配
例如:
css
/* ✖ 不推荐:万能选择器组合 */
div[class^="col-"] * {
/* * 会匹配所有子元素 */
/* 如果嵌套很深,会匹配成百上千个元素 */
padding: 10px;
}
/* ✖ 不推荐:复杂属性选择器 */
*[data-*] {
/* 扫描页面所有元素的所有data属性 */
/* 极其低效 */
}
/* ✔ 推荐:精确指定 */
.col > .col-item {
/* 只匹配直接子元素 */
/* 范围明确,效率高 */
padding: 10px;
}
如果不是特别必要,避免在高频区域大量使用 * 作为后代选择器,因为它会匹配所有嵌套元素,增加匹配范围。
在现代浏览器中,性能瓶颈更多来自布局和绘制,而不是选择器本身,但养成写"干净选择器"的习惯,可以减少潜在问题,也提升可维护性。
16.2 动画性能:尽量用 transform / opacity
动画与过渡是性能敏感区域,如果处理不当,会出现卡顿、掉帧。
16.2.1 哪些属性对性能更友好?
一般建议:优先用 transform 和 opacity 做动效。
原因:
- 它们通常只触发"合成层更新"(Composite),不需要重新布局
- 在 GPU 加速下,能以较低成本实现平滑动画
css
/* 性能影响分级 */
/* 第一级:最优 (只触发合成) */
.best-performance {
transform: translateX(100px); /* 位移 */
opacity: 0.8; /* 透明度 */
/* GPU加速,性能最佳 */
}
/* 第二级:中等 (触发重绘) */
.medium-performance {
color: red; /* 颜色 */
background: blue; /* 背景 */
box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 阴影 */
/* 需要重绘,但不用重新布局 */
}
/* 第三级:最差 (触发布局重排) */
.worst-performance {
width: 200px; /* 宽度 */
height: 100px; /* 高度 */
padding: 20px; /* 内边距 */
margin: 10px; /* 外边距 */
left: 50px; /* 位置 */
/* 整个页面可能都要重新计算 */
}
16.2.2 把位置动画改成 transform
css
/* ✖ 不推荐:使用left/top做动画 */
.box {
position: relative;
transition: left 0.3s ease; /* left会触发布局重计算 */
}
.box.move {
left: 100px; /* 每帧都要重新计算布局 */
} /* 性能较差,可能卡顿 */
/* ✔ 推荐:使用transform做动画 */
.box {
transition: transform 0.3s ease; /* transform只触发合成层更新 */
will-change: transform; /* 提示浏览器做优化 */
}
.box.move {
transform: translateX(100px); /* GPU加速,性能优异 */
} /* 不重新布局,非常流畅 */
示意对比:
txt
浏览器渲染流程对比:
left/top 动画:
改变位置 → 重新布局(Layout) → 重新绘制(Paint) → 合成(Composite)
每一帧都要走完整流程,昂贵!
transform 动画:
改变变换 → → → 合成(Composite)
跳过布局和绘制,直接GPU合成,高效!
opacity 动画:
改变透明度 → → → 合成(Composite)
同样只需要合成阶段,性能优异!
16.2.3 避免大面积、频繁的阴影变化
box-shadow 在大面积区域频繁变化会比较耗费绘制性能。
建议:
- 避免对非常大的元素做频繁阴影动画
- 尽量使用较柔和、半透明的阴影,而不是极大范围的高斯阴影
16.3 精简 CSS 与压缩策略
16.3.1 删除无用样式
在真实项目中,随着迭代,CSS 常常出现"死代码":某些类已经不再被使用。
可以借助工具:
- PurgeCSS、unCSS 等,用于分析哪些选择器在 HTML/模板中未被引用
原理大致是:
txt
扫描模板 → 记录用到的 class/id/tag
与 CSS 中选择器做对比 → 剔除未使用的部分
在前端构建流程中集成这类工具,可以显著减少产物 CSS 体积。
16.3.2 压缩与拆分
构建阶段常见优化:
- 压缩:移除空格、注释、简化颜色写法(如
#ffffff→#fff) - 拆分:按路由或模块拆分 CSS,首屏只加载必要部分
示意:
txt
styles.bundle.css → 基础 + 全局组件
home.css → 首页特定样式
dashboard.css → 后台页面特定样式
在 SPA 框架(React/Vue 等)中,这通常由构建工具自动完成,你重点是要"有这个意识":
- 尽量让页面只加载它需要的那部分 CSS
16.3.3 避免过度嵌套与重复
样式编写时尽量避免:
- Sass 里层层嵌套太深,生成类似
.a .b .c .d .e { ... }的选择器 - 同一组样式出现多次而不抽取到公共类
这不仅影响性能,更直接影响可维护性。
16.4 使用 DevTools 粗略观察性能
Chrome DevTools 提供了若干对 CSS/渲染性能有帮助的工具:
- Rendering 面板中的
Paint flashing:高亮重绘区域 - Performance 面板:录制一段用户操作,查看帧率和主要耗时
你可以做一个简单的实验:
- 写一个用 left/top 动画的位置移动效果
- 打开
Paint flashing - 再改成
transform: translate动画,对比重绘区域
不需要成为性能专家,但要有"性能嗅觉":当页面卡顿时,优先检查是否有大面积布局变化或复杂动画。
第16章从选择器、动画到 CSS 体积,给出了性能优化的一些实践建议。接下来第17章,我们会把视角拉长,看看 Sass、PostCSS 这些工具是如何协助我们管理复杂 CSS 的。
第17章 预处理与后处理
这一章解决的核心问题:
- Sass 这样的预处理器在现代前端中扮演什么角色?
- PostCSS、Autoprefixer 这些"后处理"工具主要做什么?
- 什么时候该用预处理器,什么时候用原生 CSS 变量和现代特性?
17.1 Sass 基础
Sass 是最流行的 CSS 预处理器之一,常见语法后缀有 .scss 和 .sass,其中 .scss 与 CSS 语法更接近(分号、大括号等都保留)。
预处理器的核心思想:在编译阶段扩展 CSS 的能力,比如:
- 变量
- 嵌套
- Mixin(可复用片段)
- 函数、循环等
17.1.1 变量与嵌套示例
SCSS 示例:
scss
/* SCSS:Sass的主流语法,兼容CSS写法 */
// 变量定义:$开头
$primary: #2563eb; // 主色变量
$radius-md: 12px; // 圆角变量
.btn {
padding: 0.6em 1.4em;
border-radius: $radius-md; // 使用变量
background: $primary; // 使用变量
color: #fff;
// &代表父选择器(.btn)
&:hover { // 等同于 .btn:hover
// ⚠️ darken() 在 Dart Sass 1.79+ 已废弃,会产生警告
// 旧写法(不推荐):background: darken($primary, 8%);
// 新写法:使用 color.adjust() 或 color.scale()
@use 'sass:color';
background: color.adjust($primary, $lightness: -8%); // 亮度降低8%
}
// 嵌套修饰符
&--large { // 等同于 .btn--large
font-size: 1.2rem;
}
}
编译后生成的 CSS 大致为:
css
/* 编译后的普通CSS */
.btn {
padding: 0.6em 1.4em;
border-radius: 12px; /* $radius-md 被替换为 12px */
background: #2563eb; /* $primary 被替换为 #2563eb */
color: #fff;
}
.btn:hover { /* 嵌套被展开 */
background: #1d4ed8; /* darken()函数被计算为具体值 */
}
.btn--large { /* BEM嵌套被展开 */
font-size: 1.2rem;
}
你可以看到:
- 变量
$primary在编译时被替换为实际颜色值 &:hover嵌套被展开为.btn:hover
17.1.2 Mixin 与函数
Mixin:
scss
/* Mixin:可复用的样式片段 */
// 定义mixin
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// 带参数的mixin
@mixin button-size($padding-y, $padding-x, $font-size) {
padding: $padding-y $padding-x;
font-size: $font-size;
}
// 使用mixin
.badge {
@include flex-center; // 引入flex-center的所有样式
padding: 0.1rem 0.55rem;
}
.btn-large {
@include button-size(0.8rem, 1.5rem, 1.1rem); // 传参
}
函数示例(简化):
scss
/* Sass函数:计算和返回值 */
// 定义函数,$base有默认值16
@function px-to-rem($px, $base: 16) {
@return ($px / $base) * 1rem; // 返回计算结果
}
// 颜色处理函数
@function tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
// 使用函数
body {
font-size: px-to-rem(18); // 编译为 1.125rem
line-height: px-to-rem(28); // 编译为 1.75rem
}
.light-blue {
background: tint($primary, 80%); // 80%白色 + 20%主色
}
这些都在"编译阶段"被算好,输出普通 CSS。
17.2 嵌套、变量、Mixin 的利与弊
预处理器带来便利的同时,也有一些需要注意的地方。
17.2.1 优点
scss
/* 预处理器的优点 */
// 1. 变量管理
$colors: (
primary: #2563eb,
success: #10b981,
danger: #ef4444
); // 集中管理颜色
// 2. 代码复用
@mixin card-style {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1rem;
} // 一处定义,多处使用
// 3. 嵌套结构清晰
.nav {
ul { // 视觉上符合HTML结构
li {
a { }
}
}
}
// 4. 计算能力
$base-font: 16px;
$scale: 1.25;
h1 { font-size: $base-font * $scale * $scale; } // 25px
h2 { font-size: $base-font * $scale; } // 20px
17.2.2 潜在问题
scss
/* 潜在问题和注意事项 */
// ✖ 过度嵌套问题
.page {
.container {
.content {
.article {
.paragraph {
a { } // 编译后:.page .container .content .article .paragraph a
// 太长了!难维护!
}
}
}
}
}
// ✔ 限制嵌套层级(最多3层)
.article {
&__title { } // .article__title
&__content {
a { } // .article__content a,适度即可
}
}
// ✖ Mixin膨胀问题
@mixin complex-style {
// 50行代码...
}
.item1 { @include complex-style; } // 复制50行
.item2 { @include complex-style; } // 再复制50行
// CSS体积翻倍!
// ✔ 用extend或公共类
%card-base { // placeholder选择器
// 公共样式
}
.card { @extend %card-base; } // 继承,不重复代码
一个经验法则:
用预处理器"补足"当前项目所需的能力,而不是把它当成"越复杂越好"的玩具。
17.3 PostCSS 与 Autoprefixer
PostCSS 更像是一个"CSS 处理平台",Autoprefixer 是其中最常用的插件之一。
17.3.1 Autoprefixer:自动补全厂商前缀
过去我们常常需要手动写:
css
/* Autoprefixer 自动添加厂商前缀 */
/* 你写的代码 */
.box {
border-radius: 12px;
user-select: none;
display: flex;
}
/* Autoprefixer处理后 */
.box {
-webkit-border-radius: 12px; /* 旧版Safari */
-moz-border-radius: 12px; /* 旧版Firefox */
border-radius: 12px; /* 标准属性 */
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
user-select: none; /* 标准 */
display: -webkit-box; /* 旧版Safari */
display: -webkit-flex; /* Safari */
display: -ms-flexbox; /* IE10 */
display: flex; /* 标准 */
}
js
/* browserslist 配置文件 */
// package.json 中配置
"browserslist": [
"> 1%", // 全球使用率 > 1%
"last 2 versions", // 每个浏览器最新两个版本
"not dead", // 排除已停止维护的浏览器
"not ie 11" // 排除IE11
]
// 或者 .browserslistrc 文件
> 0.5% # 使用率大于0.5%
last 2 Chrome versions # Chrome最新两版
last 2 Firefox versions # Firefox最新两版
last 2 Safari versions # Safari最新两版
not dead # 排除死掉的浏览器
// Autoprefixer会根据这个列表决定:
// - 需要添加哪些前缀
// - 可以删除哪些不再需要的前缀
17.3.2 其它常见 PostCSS 插件
js
/* 常见PostCSS插件及作用 */
// postcss.config.js
module.exports = {
plugins: [
// 1. Autoprefixer:自动添加厂商前缀
require('autoprefixer'),
// 2. CSSnano:压缩优化CSS
require('cssnano')({
preset: 'default', // 移除注释、压缩空格
// 合并规则、简化颜色等
}),
// 3. postcss-preset-env:使用未来CSS特性
require('postcss-preset-env')({
stage: 0, // 启用实验性特性
features: {
'nesting-rules': true, // CSS嵌套
'custom-properties': false, // 禁用(已有原生支持)
}
}),
// 4. purgecss:移除未使用的CSS
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html'], // 扫描这些文件
safelist: ['active', 'hidden'] // 保留这些类
})
]
}
在现代前端工程里,PostCSS 通常集成在构建链路中(如 webpack、Vite),你配置好插件后,它在打包时自动处理 CSS。
17.4 构建工具与现代前端中的位置
把整个链路简化成一张图:
txt
CSS 工程化流程图:
开发阶段:
开发者编写
↓
Sass/Less/Stylus ← 预处理器
(变量、嵌套、Mixin) 提供开发便利
↓ 编译
标准 CSS
↓
构建阶段:
PostCSS ← 后处理器
│ 处理兼容性
├─ Autoprefixer (自动前缀)
├─ CSSnano (压缩优化)
└─ PurgeCSS (移除无用)
↓
打包工具
(Webpack/Vite) ← 模块化管理
↓ 代码分割
最终CSS产物
(压缩、优化、兼容)
↓
部署到生产环境
现代原生 CSS 的能力越来越强:
css
/* 现代CSS原生特性 🆕 */
/* 1. CSS变量(已广泛支持) */
:root {
--primary: #2563eb; /* 运行时变量 */
} /* 可动态修改! */
/* 2. CSS嵌套(正在普及) */
.card {
&__title { } /* 原生支持嵌套 */
&:hover { } /* 不再需要Sass */
}
/* 3. @layer 层叠层(2022年已全面落地,Chrome 99+/Firefox 97+/Safari 15.4+) */
@layer base, components, utilities;
@layer components {
.btn { } /* 明确控制层级 */
}
/* 4. @container 容器查询(2023年已全面落地,Chrome 105+/Firefox 110+/Safari 16+) */
.card {
container-type: inline-size;
}
@container (min-width: 400px) {
.card__title { } /* 基于容器而非视口 */
}
/* 5. :is() / :where() 选择器 */
:is(h1, h2, h3) { /* 简化多选择器 */
color: var(--primary);
}
因此,新的项目可以考虑:
- 优先使用现代 CSS 原生特性
- 在确实需要时,引入少量预处理器能力
工具的目标是"让你的 CSS 更易维护、更好适配浏览器",而不是增加复杂度本身。理解它们的角色之后,你可以针对项目规模、团队情况来选择合适的组合。
第六篇从层叠与优先级、到性能优化,再到预处理与后处理工具,带你从"会写 CSS"更进一步,理解浏览器与工程化的基础。到这里,你已经具备了构建中大型 CSS 代码库的核心知识储备,接下来可以把更多精力放在"真实项目实践"与"团队协作规范"上。