CSS 到底是怎么工作的?从浏览器加载 CSS 开始讲清楚

CSS1周前发布 admin
128 0

写了这么多年 CSS,你有没有想过一个问题:你写的那些 .class { color: red; },到底是怎么变成屏幕上一块红色文字的?

很多人对 CSS 的理解停留在”选择器 + 属性 = 样式”这个层面,能写但说不清楚背后发生了什么。今天就把这条链路从头到尾捋一遍——从浏览器怎么拿到 CSS 文件,到样式最终怎么”画”到屏幕上。

搞懂这个过程,很多性能优化的建议(比如”CSS 放头部”、”避免深层嵌套选择器”)就不再是死记硬背的规则,而是自然而然的结论。

第一步:CSS 是怎么被浏览器拿到的

浏览器渲染一个页面,第一件事是请求 HTML。HTML 解析器从上往下一行行地解析文档,这个过程中会遇到 <link> 标签,也可能遇到 <style> 标签。

  • <style> 标签里的 CSS 是内联的,不需要额外请求,解析到就能直接用。
  • <link rel="stylesheet"> 引用的是外部文件,浏览器发现这个标签后,会立刻发起一个网络请求去下载这个 CSS 文件,而且这个请求和 HTML 解析是并行的——浏览器不会傻等 CSS 下载完才继续解析后面的 HTML。

这里有个关键点很多人会搞混:CSS 的下载不阻塞 HTML 解析,但会阻塞渲染。 也就是说,浏览器可以一边下载 CSS,一边继续把 HTML 解析成 DOM 树,但在 CSS 没有下载解析完成之前,页面不会显示任何内容。

这么设计的原因很朴素:浏览器不想先给用户看一个没有样式、乱糟糟的页面,再”闪一下”变成有样式的页面(这种现象叫 FOUC,Flash of Unstyled Content)。

所以浏览器选择把渲染这件事往后拖,等样式信息齐全了再画。

这也是为什么大家都建议把 CSS 放在 <head> 里尽早引入——越早发起请求,浏览器就能越早开始下载,减少页面”空白等待”的时间。

反过来,如果 CSS 放在 <body> 最后面,那前面所有内容都要等到最后才能一起渲染出来,体验会很差。

第二步:CSS 文本是怎么变成浏览器能理解的结构的

拿到 CSS 文本之后,浏览器不会直接拿着这一大坨字符串去套用到页面上,它需要先把这坨文本解析成一种结构化的数据,业内一般管这个叫 CSSOM(CSS Object Model)。

这个过程和 HTML 解析成 DOM 树其实是类似的思路:

  1. 浏览器把 CSS 文本按语法规则拆解成一个个 token(比如选择器、属性名、属性值)。
  2. 把这些 token 组织成规则(rule),每条规则包含”选择器 + 一堆声明”。
  3. 把所有规则按照层级关系组织成一棵树——这就是 CSSOM。

CSSOM 和 DOM 一样是一棵树状结构,但它描述的不是页面的元素,而是样式规则之间的继承和层叠关系。比如 body 上设置的 font-size,理论上会往下传递给它的子元素,这种继承关系就体现在 CSSOM 树里。

有一点值得单独说一下:CSSOM 的构建是渲染阻塞的,而且比 HTML 解析要”娇气”得多。因为 CSS 规则之间存在层叠和覆盖关系(后面的规则可能覆盖前面的),浏览器必须等所有 CSS 规则都处理完,才能确定每个元素最终应该用哪些样式。

这和 HTML 解析不一样——HTML 可以一边解析一边把已经解析出来的部分交给后续流程处理,但 CSSOM 必须等”全部到齐”才能用。这也是为什么体积过大的 CSS 文件、或者层层嵌套的 @import 会明显拖慢首屏时间。

第三步:样式是怎么”套”到每个元素身上的——层叠与选择器优先级

有了 CSSOM,浏览器接下来要解决的问题是:每一个 DOM 元素,到底应该应用哪些样式?

这一步涉及到 CSS 里最容易让人困惑,但其实逻辑很清晰的部分——层叠(Cascade)。简单说,当多条规则同时命中同一个元素的同一个属性时,浏览器需要一套确定性的算法来判断”听谁的”。这套算法大致按下面的顺序判断:

  1. 来源和重要性:用户自定义样式、浏览器默认样式、!important 标记的规则,谁的优先级更高是有明确规定的。一般来说带 !important 的作者样式优先级最高。
  2. 选择器优先级(specificity):内联样式 > ID 选择器 > 类选择器/属性选择器/伪类 > 元素选择器/伪元素。这也是为什么 #header .title 会比单独一个 .title 更”强势”。
  3. 书写顺序:如果优先级完全一样,后写的规则会覆盖先写的规则——这也是为什么同名的两条规则,后面那条会生效。

浏览器会针对 DOM 树上的每一个节点,把命中它的所有规则拿出来,按上面这套逻辑排出胜负,最终确定这个节点身上每一个 CSS 属性的最终取值。

这一步结束后产生的结果,通常被称为”计算样式”(Computed Style)——也就是说,不管你写没写某个属性,每个元素身上的每一个 CSS 属性,到这一步都有了一个确定的值(要么是你写的,要么是继承来的,要么是浏览器给的默认值)。

第四步:渲染树的诞生——DOM 遇上 CSSOM

有了 DOM(页面结构)和 CSSOM(样式规则),浏览器会把两者结合,生成一棵新的树,叫做渲染树(Render Tree)

这一步做的事情可以理解成:遍历 DOM 树上的每一个节点,把它对应的计算样式挂上去,同时过滤掉那些不需要显示的节点。哪些节点不会进入渲染树?

  • display: none 的元素——注意,这和 visibility: hidden 不一样。visibility: hidden 的元素虽然看不见,但依然占据空间,依然会出现在渲染树里;而 display: none 的元素相当于压根不存在,不会占用任何布局空间,也不会进入渲染树。
  • <head><script><meta> 这类本身就不用于显示的节点。

渲染树里的每一个节点,都携带了它最终应该长什么样的全部信息:字体多大、颜色是什么、该占多少地方的初步依据等等。但这时候浏览器还不知道每个元素具体应该画在屏幕的哪个坐标、占多大的实际像素——这是下一步要解决的问题。

第五步:布局(Layout / Reflow)——算出每个元素在哪、多大

渲染树只知道”有哪些东西要显示、它们的样式是什么”,但不知道具体的位置和尺寸。布局阶段(也叫 Reflow)要做的,就是把这些抽象的样式信息,换算成具体的像素坐标和宽高。

这一步会从渲染树的根节点开始,递归地计算每个节点的盒模型(box model)——也就是 content、padding、border、margin 各占多少空间,以及这个盒子相对于视口(viewport)的具体坐标。

举个例子,一个设置了 width: 50% 的元素,浏览器要先知道它父元素的实际像素宽度,才能算出这个 50% 具体是多少像素;而流式布局中,一个元素的宽度变化,往往会连锁影响它后面兄弟元素的位置——这也是为什么”重排”的影响面往往比想象中大,一个节点的尺寸变化可能引发一连串节点重新计算位置。

第六步:绘制(Paint)——把”抽象的盒子”变成实际的像素

布局阶段确定了每个元素在哪、多大,绘制阶段要做的,是把这些盒子实际”画”出来——包括背景色、文字、边框、阴影、图片等等视觉细节,转换成屏幕上的像素点。

现代浏览器在这一步通常还会做进一步的优化,比如把页面拆分成多个图层(Layer)分别绘制,再交给 GPU 合成(Composite)。

transformopacity 这类属性之所以被认为是”性能友好”的动画属性,原因就在这里——改变它们往往只需要重新合成图层,而不需要重新走一遍布局和绘制的全流程,代价小得多。

相比之下,改变 widthtop 这类会影响盒子几何形状的属性,通常会触发重新布局,连带着重新绘制,开销要大得多。

串起来看:一次完整的链路

把上面几步串起来,一个 CSS 文件从被浏览器发现,到最终变成屏幕上的像素,大致是这样一条链路:

发现 <link> 标签 → 并行下载 CSS 文件 → 解析成 CSSOM → 结合 DOM 计算每个元素的最终样式并生成渲染树 → 布局阶段算出每个元素的位置和尺寸 → 绘制阶段把盒子画成像素 → 必要时合成图层输出到屏幕。

理解了这条链路,很多常见的”最佳实践”其实都能自己推导出来,而不需要死记:

  • CSS 要尽早加载,因为它阻塞渲染,不是阻塞解析。
  • CSS 文件不宜过大过复杂,因为 CSSOM 构建必须等”全部到齐”。
  • 选择器不要嵌套太深,一方面影响可维护性,另一方面浏览器匹配选择器也是有成本的。
  • 做动画优先用 transform/opacity,因为它们能跳过布局和绘制,直接走合成阶段,更省性能。

CSS 表面上是”写样式”,但背后其实是浏览器一整套从文本解析、树结构构建、样式计算、几何布局到像素绘制的工程流水线。把这条流水线的每一站都想明白,你对 CSS 的理解就不再是”背规则”,而是真正”懂原理”了。

© 版权声明

相关文章

暂无评论

none
暂无评论...