从样式表到屏幕:CSS在浏览器处理流程中的位置
从下载到像素:深入理解CSS——DOM、CSSOM与渲染树的构建方式,样式级联的运行过程,以及哪些样式来源会为每个元素竞争优先权。
大多数开发者都是凭感觉编写 CSS:修改某个属性,重新加载页面,查看效果。这种方法在规则突然无法应用、页面显示未加样式的内容,或是“简单”的样式更改导致滚动卡顿时就会失效。一旦你了解浏览器在接收到样式表到绘制像素之间究竟做了什么,这些问题的解决就会变得容易得多。
本指南将从宏观层面梳理这一过程。你将了解到浏览器如何将 HTML 转换为 DOM,样式表如何变为 CSSOM,二者如何结合形成渲染树,级联规则如何解决冲突的样式声明,以及哪些样式来源会为每个元素竞争优先权。这也是常见的面试问题,通常表述为“CSS 在底层是如何工作的?”,下面的答案能为你提供结构化的回答思路。
第一步:HTML 转换为 DOM
当你打开一个URL时,浏览器首先接收HTML文档。它会从上到下解析标记内容,并在此过程中构建文档对象模型。DOM是一棵代表整个文档的树结构:每个元素都是一个节点,这些节点以父节点、子节点和兄弟节点的关系相互关联,就像家谱一样。HTML中描述的所有内容都存在于这个结构中,而JavaScript也正是读取并修改这些内容的。
解析是逐步进行的。浏览器不会等到整个文件下载完毕才开始构建节点,因此它能够在文档下载完成之前就发现其他资源。
第二步:样式表转化为CSSOM
在解析HTML时,浏览器会遇到样式表,无论它们是通过中的<link rel="stylesheet">链接引入的,还是嵌入在<style>元素中的,随后也会开始获取并解析这些样式表。CSS会被解析成一种树形结构,即CSS对象模型,或CSSOM。它对于样式的作用,就如同DOM对于标记内容的作用一样。
将CSS转换为可用于元素的样式,比将HTML转换为节点需要更多的处理工作。其中有两项关键任务:
- 解决冲突。通常会有多个声明针对同一个元素上的同一属性。浏览器会通过一种称为级联的算法来解决这些冲突。
2em、50% 或 inherit,但这些值目前还无法被布局引擎直接使用。浏览器会将它们转换为具体的数值。严格来说,CSSOM 是样式表的解析结果,而级联规则和数值计算则发生在浏览器为每个元素计算样式时。不过为了便于理解,可以将其视为“CSS被解析、冲突得到解决、数值最终确定,然后将结果应用到元素上”。
一个实际后果是:由于浏览器在渲染任何内容之前都需要样式,因此位于页面头部区域的样式表必须等待加载并解析完成才能被应用。这就是为什么庞大的、加载缓慢的样式表会延迟页面的首次渲染,也解释了为何保持关键CSS文件的大小适中对性能如此重要。
第三步:DOM与CSSOM合并为渲染树
当标记被解析为DOM结构,样式被解析为CSSOM结构后,浏览器会将二者合并成渲染树。渲染树包含实际要显示的节点,每个节点都对应其计算后的样式。那些不会产生视觉输出的节点,比如<head>中的内容或设置了display: none的元素,则会被排除在外。
此时浏览器已经知道要绘制什么以及每个元素的样式设置,但还不知道各元素的位置及大小。
第四步:布局与视觉格式化模型
为将带样式的节点转换为定位框,浏览器会遵循CSS规范中所定义的视觉格式化模型。该部分CSS规范描述了如何为笔记本电脑或手机屏幕等视觉媒介排列文档树中的元素,涵盖了框模型、块级与行内格式化、浮动、定位以及决定每个框大小和位置的其他规则。
一旦布局为每个框计算出几何参数,浏览器就会对其进行绘制,填充文本、颜色、边框、图像和阴影,最终结果便显示在屏幕上。
整体流程一览
将各个阶段串联起来,就形成了从标记到像素的简单流程。每根箭头背后都隐藏着大量工作,但正是这种顺序对于分析错误和性能问题至关重要:
HTML
↓
DOM
↓
CSS
↓
CSSOM
↓
DOM + CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels on the Screen
实际浏览器会并行执行这些步骤,并添加更多操作(例如合成层),之后修改样式可能会迫使浏览器根据所改属性重新计算样式、调整布局或重绘页面。如需详细了解这些开销,可参阅每次CSS修改会给浏览器带来多少成本。
为何声明会冲突
本指南的其余部分将重点介绍两个CSS处理任务中的第一个:冲突解决。负责这一工作的算法是级联规则。它会合并所有应用于文档的样式表,当多个声明为同一元素设置相同的属性时,会决定哪个声明生效。
冲突在所难免,不仅因为您的样式表可能在两个地方为链接设置了color属性。样式来自多个独立的来源,这些来源被称为“规则源”,它们会同时作用于相同的元素。
开发者自定义样式
这些是由您和您的团队编写的声明:即您的样式表、<style>标签块以及内联的style属性。在大多数网站上,它们是规则来源中占比最大的部分。
用户自定义样式
查看页面的用户也可以影响样式。浏览器允许用户调整默认字体大小等设置,部分浏览器还支持自定义用户样式表或可注入样式的扩展程序。这些偏好设置对无障碍访问尤为重要,因为它们能让视力不佳或有阅读困难的用户根据自身需求调整页面显示。
用户代理样式
最后,浏览器(即用户代理)会自带默认样式表。正因如此,没有应用样式的 <a> 元素会显示为蓝色并带有下划线,标题会以粗体显示且字号大于正文,<body> 则会有较小的边距。这些默认设置被称为用户代理样式。
当级联规则合并了这三种样式来源后,同一个元素上的同一属性很容易出现多个相互冲突的值,此时浏览器需要一种确定性的方法来做出选择。
级联决策机制
级联会按照固定的顺序准则比较相互冲突的声明,只有当前一个准则无法分出胜负时才会考虑下一个准则:
- 来源与重要性。 声明的来源,以及是否标记了
!important。 - 特异性。 选择器针对元素的精确程度;ID选择器的优先级高于类选择器,类选择器又高于类型选择器。
- 出现顺序。 若其他所有条件均相同,则出现较晚的声明具有优先权。
来源的优先级排序
对于第一个准则,经典的优先级顺序从高到低如下:
- 标记了
!important的用户声明。 - 标记了
!important的开发者声明。 - 普通的开发者声明。
注意这其中的含义。您的常规样式会覆盖用户的常规偏好设置以及浏览器的默认设置,而这正是让您能够设计页面的基础。但 !important 会改变用户与设计者之间的优先级顺序:那些确实需要更大字体或更高对比度的用户可以将该偏好标记为重要,从而凌驾于您的 !important 规则之上。浏览器的默认设置则处于最底层,只有在没有其他规则指定时才会生效。
现代CSS进一步完善了这一机制。当前的级联规则还考虑了级联层(@layer)、通过动画和过渡效果设置的样式,以及用户代理的!important声明——后者的优先级高于所有其他重要声明。上述简化的列表仍能体现日常工作中最关键的关系;如需了解完整的优先级顺序,请参阅之前链接的MDN级联参考文档。
特异性与来源顺序值得单独详细探讨,包括选择器权重的比较方式,以及为何!important往往带来的问题多于解决的问题。相关内容可在级联如何确定最终样式一文中找到。
掌握这些知识的好处
理解这一流程能改变你调试和编写样式的方式:
- 不适用的规则几乎总是级联丢失导致的。了解起源顺序、特异性以及规则来源的先后顺序,就能知道该从何处着手解决问题,而无需使用
!important。 - 未应用样式或样式应用过晚的内容,源于样式表对渲染的阻塞作用以及那些在首次渲染之后才加载的样式。
- 不流畅的交互体验往往源于迫使布局或绘制操作重新执行的更改,一旦知道某个属性会影响哪个阶段,就能避免这类问题。
- 易于维护的CSS通常是特异性较低且可预测、规则来源顺序清晰的CSS,这样既方便浏览器处理,也便于团队成员理解。
总结
浏览器将HTML转换为DOM,将样式表转换为CSSOM,再将二者结合成由可显示的、已应用样式的节点构成的渲染树,最后通过视觉格式化模型来布局这些元素并对其进行绘制。在CSS处理阶段,级联规则是首要的筛选机制:它首先合并作者样式、用户样式和用户代理样式,按照来源与重要性、具体性以及代码出现顺序来解决各种冲突。关于如何将最终确定的样式值转换为布局引擎能够使用的具体数值,可在浏览器如何在布局前解析CSS值一文中了解。