渲染流程(下):HTML、CSS和JavaScript是如何变成页面的|浏览器篇
# 渲染流程(下):HTML、CSS和JavaScript是如何变成页面的
30 秒速记
- 页面内容提交给渲染引擎后,首先解析
HTML,生成浏览器可处理的DOM结构。 - 随后结合
CSS样式表,为DOM中的节点计算最终样式。 - 布局阶段计算元素的几何位置,并把相关结果记录到布局树中。
- 这三步分别解决结构、样式和几何信息问题,存在明确的处理依赖,不能任意交换顺序。
- 给定原文只回顾了渲染流水线的前三个阶段,不能据此展开后续绘制、合成等具体机制。
页面交给渲染引擎后,会依次生成 DOM、计算最终样式,再完成布局并记录元素的几何信息。 这个顺序不能随意交换,因为样式匹配依赖页面结构,而尺寸和坐标又依赖结构与计算后的样式。比如父容器宽 400px、子元素宽 50% 时,布局阶段才能算出子元素约 200px 的使用宽度。需要注意,DOM 节点和布局对象并非一一对应,最终不参与布局的节点不会形成普通可见盒子;这里也只讨论前三个阶段,不延伸到绘制和合成。
在上篇文章中,我们介绍了渲染流水线中的DOM生成、样式计算和布局三个阶段,那今天我们接着讲解渲染流水线后面的阶段。
这里还是先简单回顾下上节前三个阶段的主要内容:在HTML页面内容被提交给渲染引擎之后,渲染引擎首先将HTML解析为浏览器可以理解的DOM;然后根据CSS样式表,计算出DOM树所有节点的样式;接着又计算每个元素的几何坐标位置,并将这些信息保存在布局树中
原理拆解: 前三个阶段形成一条有依赖关系的数据转换链。HTML 解析负责识别元素、属性和文本之间的父子关系,生成 DOM;样式计算再根据样式表、继承和层叠规则,为相关节点确定最终样式;布局阶段读取结构和计算后的样式,计算元素的尺寸及位置,并把参与布局的节点组织到布局树中。结构尚未确定时无法完整匹配样式,尺寸规则尚未确定时也无法可靠计算几何位置,因此顺序不能任意交换。
具体例子: 对一个包含父容器和子元素的页面,DOM 先保存两者的层级关系;若样式规定父容器宽度为 400px、子元素宽度为 50%,样式计算确定各自采用的规则,布局阶段才能结合包含块计算出子元素约 200px 的使用宽度及其坐标。某些节点虽然存在于 DOM,但若最终样式使其不参与布局,就不会以普通可见盒子的形式出现在布局树中。
边界与反例: DOM 节点与布局对象不能简单视为一一对应:不可见节点可能不生成布局对象,样式生成的视觉盒子也不一定直接对应一个普通元素节点。修改文本或节点结构可能影响解析后的结构;修改颜色通常只需更新相关样式结果,而改变宽度、字体尺寸等几何属性还可能使布局失效。具体失效范围由浏览器实现和元素关系决定,不能假定每次修改都会重算整棵树。
工程验证: 可在开发者工具的元素面板检查 DOM 层级和最终计算样式,再读取元素的 getBoundingClientRect() 观察布局后的尺寸与坐标。依次切换 display、宽度或字体尺寸,比较节点是否仍有几何盒子以及数值如何变化,即可分别验证结构、样式与布局信息并非同一层数据。
面试官追问
追问 1页面里的按钮能在 DOM 面板中找到,但设置 display: none 后布局树里没有对应可见盒子,这是否说明 HTML 解析失败?
这不能说明解析失败,因为节点已经存在于 DOM,缺少布局对象可能是最终样式使其不参与布局。DOM 节点与布局对象并非一一对应;还应检查计算样式和几何信息,不能仅凭布局树缺失回退到解析阶段。
追问 2一个宽度为 400px 的父容器中,子元素声明 width: 50%,你会怎样验证浏览器在哪个阶段得到约 200px 的使用宽度?
样式计算先确定父子节点采用的宽度规则,布局阶段再结合包含块算出子元素的实际尺寸和坐标。可在开发者工具检查最终样式,并用 getBoundingClientRect() 读取布局结果;若还有其他约束,不能仅凭声明值保证结果恰为 200px。
追问 3代码评审中有人想把布局放在样式计算之前,以便更早获得首屏坐标,这个顺序为什么站不住脚?
布局依赖已经确定的结构和最终样式,宽度、字体尺寸等规则未明确时无法可靠计算几何位置。样式匹配本身又依赖 DOM 的元素与父子关系,因此数据转换顺序不能任意交换;具体实现可优化执行范围,但不能消除这些依赖。
追问 4线上元素层级和颜色都正确,但弹窗尺寸与位置异常,你会如何在 DOM、样式和布局三层之间定位?
先确认 DOM 层级,再检查影响几何的最终计算样式,最后用 getBoundingClientRect() 对照实际尺寸与坐标。颜色正确只能说明部分样式生效,不能证明宽度、字体或包含块规则正确;失效范围还取决于元素关系和浏览器实现。
追问 5交互团队要高频切换文字颜色和容器宽度,并认为两者更新成本完全相同,你会指出什么差异?
修改颜色通常只需更新相关样式结果,而改变宽度或字体尺寸可能让已有布局失效并重新计算几何信息。两者都可能触发样式变化,但对布局树的影响并不相同;具体重算范围不能脱离浏览器实现和元素依赖关系武断确定。
# 分层
30 秒速记
- 布局完成后不会直接绘制;渲染引擎还会遍历布局树,建立用于组织绘制与合成的图层树。
- 布局节点与图层不是一一对应:没有独立图层的节点会归属祖先图层,但每个节点最终都会直接或间接落入某一层。
- 按原文描述,具有层叠上下文特征的元素可能被提升,例如定位与层叠排序、透明效果或
CSS滤镜相关元素。 - 需要裁剪的内容也可能形成独立图层;可滚动区域中的内容与滚动条可能分别分层。
- 分层能支持复杂变换、滚动和垂直方向的叠放,但图层数量增加也意味着后续绘制、栅格化与合成工作随之增加;具体提升规则取决于浏览器实现与版本。
布局完成后,渲染引擎会遍历布局树,为需要独立处理的节点建立图层树。 布局节点和图层不是一一对应的,没有专属图层的节点会归到祖先图层,但最终都会直接或间接属于某一层。像层叠上下文、透明效果、CSS 滤镜或需要裁剪的内容,都可能触发独立分层,可滚动内容和滚动条也可能分开处理。分层方便实现变换、滚动和 z 轴叠放,不过层数越多,后续绘制、栅格化和合成的工作也越多,具体规则还要看浏览器实现。
现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。
要想直观地理解什么是图层,你可以打开Chrome的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示

从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面,你可以参考下图

现在你知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如上图中的span标签没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
页面是个二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。你可以结合下图来直观感受下:

从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。
第二点,需要剪裁(clip)的地方也会被创建为图层。
不过首先你需要了解什么是剪裁,结合下面的HTML代码:
<style>
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
</style>
<body>
<div >
<p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
<p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
<p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p>
</div>
</body>
在这里我们把div的大小限定为200 * 200像素,而div里面的文字内容比较多,文字所显示的区域肯定会超出200 * 200的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在div区域,下图是运行时的执行结果

出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:

所以说,元素有了层叠上下文的属性或者需要被剪裁,满足这任意一点,就会被提升成为单独一层。
面试官追问
追问 1商品详情页里有 300 个 span,同事断言布局树有 300 个节点就一定会生成 300 个图层,你会怎么反驳?
布局树节点与图层不是一一对应,普通 span 没有专属图层时会从属于父节点所在的层。渲染引擎遍历布局树生成图层树,最终每个节点都会直接或间接归属某层,但不能据节点数量推算图层数量。
追问 2线上弹窗设置了 z-index: 999999,仍被带透明度和定位的导航栏盖住,值班同学继续加大数值,你会先查什么?
先在 Layers 面板确认两者所属图层及其层叠关系,再检查各自是否形成了不同的层叠上下文。z-index 受所在层叠上下文约束,并非全页面统一比较,继续放大数值可能无效,也无法替代对分层结构的定位。
追问 3代码评审中有人给 500 个商品卡片统一加透明度和 CSS 滤镜,理由是让每个卡片独立成层,这个改动应如何判断?
这些属性可能让元素具有层叠上下文并被提升为独立图层,因此应先用 Layers 面板验证实际分层。大量节点成层会改变图层树和后续绘制、栅格化、合成的工作量,不能把提升图层当作无条件优化。
追问 4一个原本完整展示的评论区改成 200×200 且 overflow: auto 后,文字和滚动条出现异常,你会怎样沿分层阶段排查?
裁剪需求会影响图层创建,原文所述实现会为被裁剪的文字部分建层,出现的滚动条也可能单独成层。应检查尺寸、溢出设置以及 Layers 中的归属和叠加顺序;具体图层组织属于对应 Chromium 实现,不能外推为所有浏览器的固定规则。
追问 5设计师要求用 3D transform、透明度和滤镜实现卡片翻转,开发者想为了减少图层全部改成普通定位,你会如何取舍?
这些复杂效果正是渲染引擎建立专用图层的重要场景,直接取消可能破坏预期的三维和叠加表现。应依据视觉需求与实际图层树决定保留范围,并验证裁剪和层叠关系;图层过多有后续成本,过度合并也可能使效果难以正确组织。
