前言
当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环的作用下,渲染主线程会取出消息队列的渲染任务,开启渲染流程。(不知道事件循环的同学可以去我的这篇文章先了解一下事件循环 前端人必须理解的事件循环)
整个渲染流程分为多个阶段,分别是:HTML解析,样式计算,布局,分层,绘制,分块,光栅化,画
每个阶段都有明确的输入输出,上一个阶段的输出会作为下一阶段的输入。
这样,整个渲染过程就形成了一套组织严密的生产流水线。
一、解析HTML
会生成两棵树,一棵是DOM树,另一棵是CSSOM树。
解析 HTML 生成 DOM
-
词法分析
- 当浏览器接收到 HTML 文本时,首先进行词法分析。它将 HTML 文本分解成一个个的标记(tokens),例如
<html>
、<body>
、<p>
等标签以及它们包含的文本内容、属性等都被视为不同类型的标记。例如,对于 HTML 代码<p class="my - paragraph">Hello, world!</p>
,词法分析器会识别出<p>
开始标签标记、class="my - paragraph"
属性标记、Hello, world!
文本标记和</p>
结束标签标记。
- 当浏览器接收到 HTML 文本时,首先进行词法分析。它将 HTML 文本分解成一个个的标记(tokens),例如
-
语法分析
- 基于词法分析得到的标记,语法分析器会构建出 DOM 树。它根据 HTML 的语法规则将标记组合在一起。在构建 DOM 树的过程中,会识别出元素之间的嵌套关系。例如,在一个完整的 HTML 页面中,
<html>
标签是根节点,<body>
标签是<html>
的子节点,而<p>
标签可能是<body>
的子节点。对于以下简单 HTML:
- 基于词法分析得到的标记,语法分析器会构建出 DOM 树。它根据 HTML 的语法规则将标记组合在一起。在构建 DOM 树的过程中,会识别出元素之间的嵌套关系。例如,在一个完整的 HTML 页面中,
<html>
<body>
<h1>Title</h1>
<p>Paragrap</p>
</body>
</html>
- 会构建出一个以
html
为根的 DOM 树,其结构如下:
解析 CSS 生成 CSSOM
-
加载 CSS
-
浏览器会通过多种方式加载 CSS,包括内联样式(在 HTML 元素的
style
属性中定义)、<style>
标签内的样式和通过<link>
标签引用的外部 CSS 文件。例如:- 内联样式:
<p style="color: red;">This is a red paragraph.</p>
<style>
标签样式:
- 内联样式:
-
<style>
h1 {
font - size: 24px;
}
</style>
- 外部 CSS 文件(通过
<link>
):<link rel="stylesheet" type="text/css" href="styles.css">
-
解析 CSS 规则
- 对于加载的 CSS 内容,浏览器会进行解析。它将 CSS 样式规则解析成可以被理解和处理的格式。例如,对于 CSS 规则
p { color: blue; margin - bottom: 10px; }
,浏览器会识别出选择器p
以及对应的样式声明{ color: blue; margin - bottom: 10px; }
。
- 对于加载的 CSS 内容,浏览器会进行解析。它将 CSS 样式规则解析成可以被理解和处理的格式。例如,对于 CSS 规则
-
构建 CSSOM
- 基于解析后的 CSS 规则,浏览器构建 CSSOM。CSSOM 是一个树形结构,其中每个节点代表一个 CSS 规则或样式声明。在 CSSOM 树中,样式会根据选择器的特异性(specificity)和层叠规则进行组织。例如,如果有以下 CSS 规则:
body {
font - family: Arial;
}
p {
color: red;
}
- CSSOM 会将这些规则组织起来,并且当应用到 DOM 元素时,会根据特异性等规则确定最终应用的样式。如果一个
<p>
元素在<body>
内,它会继承body
的font - family
样式,同时应用自身的color: red
样式。
body h1{
font - size:3em;
color:Red;
}
下面是CSSOM树
Html解析过程中遇到CSS代码怎么办?
为了提高解析效率,浏览器会启动一个预解析器率先下载和解析
CSS
如果主线程解析到LINK位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后面的HTML。这是因为下载和解析CSS是在预解析线程上进行的,这就是CSS不会阻塞HTML解析的根本原因。
HTML解析过程中如果遇到JS代码怎么办?
渲染主线程遇到JS代码时必须暂停一切行为,等待下载执行完了之后才可以继续解析HTML,预解析线程可以帮忙分担一点下载JS的任务
如果主线程解析到Sctipt位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析完成后,才可以继续解析HTML,这是因为JS的执行过程可能会改变当前的DOM树,所以DOM树生成必须暂停。这就是JS会阻塞HTML的原因
为什么Js代码不会生成树?
因为JS代码只需要执行一遍就OK了,后续的步骤用不到JS,DOM树和cssom树会在后续的步骤中用到。
DOM 和 CSSOM 的关联
-
一旦 DOM 和 CSSOM 都构建完成,浏览器会将两者关联起来。它会根据 CSSOM 中的样式规则,将样式应用到 DOM 中的相应元素上。这个过程会遍历 DOM 树中的每个元素,查找匹配的 CSSOM 规则,并计算出最终的样式。例如,当浏览器渲染一个
<p>
元素时,它会在 CSSOM 中查找适用于p
元素的样式,然后将这些样式应用到 DOM 中的<p>
元素节点,从而确定该元素在页面上的视觉呈现,如字体颜色、大小、背景等。DOM 和 CSSOM 的关联就是样式计算
二、样式计算
样式计算的结果是为每个元素确定最终的样式属性
首先会解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树,然后将两者合并生成渲染树(Render Tree)。在这个过程中,样式计算起着关键作用。它会处理 CSS 规则的优先级、继承关系等复杂情况,为每个元素确定最终的样式属性。
这一步完成之后,会得到一颗带样式的DOM树
三、布局
接下来是布局,布局之后会得到布局树。 渲染树和布局树的构建都依赖于 DOM 树和 CSSOM 树。渲染树主要是将 DOM 树中的可见元素与 CSSOM 树中的样式信息相结合,确定每个元素的显示样式。而布局树在渲染树的基础上,更侧重于计算每个元素的几何属性,如位置和大小。可以说,布局树是对渲染树的进一步细化处理,用于确定页面元素在屏幕上的具体布局。 布局完成后会得到布局树。 布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,比如节点的宽高、相对包含块的位置。 元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值(比如 position 被设置为 absolute 或 fixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。
大部分时候,DOM 树和 Layout 树并非一一对应的,为什么?
比如某个节点的 display 设置为 none,这样的节点就没有几何信息,因此不会生成到 layout 树。有些使用了伪元素选择器,虽然 DOM 树中并不存在这些伪元素节点,但它们需要显示在浏览器上,所以会生成到 layout 树上。还有匿名行盒、匿名块盒等等都会导致 DOM 树和 layout 树无法一一对应。
div::before {
content: '';
}
<div></div>
四、分层 Layer
浏览器拿到布局后的 layout tree 会思考一些事情。大多数页面不是绘制后静止的,经常会有一些动画、用户点击后一些交互处理等,需要经常刷新页面。但如果整个页面直接重新刷新,这个性能开销是很大的。能不能提高效率?能,于是现在浏览器支持了分层。
主线程会使用一套复杂的策略对整个布局树进行分层。 分层的好处在于,将来在某一个层改变后,仅会对该层进行后续处理,从而提高效率。
滚动条、堆叠上下文有关的(z-index、transform、opacity )样式都会或多或少影响分层结果,也可以通过 will-change 属性更大程度的影响分层的结果。
html
代码解读
复制代码
<div>
100个<p>Lorem</p>
</div>
可以看到默认情况下浏览器对该代码分了2层。为什么滚动条需要单独设置一个 layer?因为100个 p 标签一定会超出一屏,所以会存在滚动条,且用户可能会高频滚动去查看内容,为了高效渲染,所以将滚动条单独设置了一个层。
假设我们已经通过技术手段得知某些内容经常会改变,但直接页面整体重刷会很耗费资源,那如何只对某一块区域设置一个 layer 呢?通过 will-change: transform
告诉浏览器,这块区域的 transform 属性经常会变。
will-change
是一个用于通知浏览器某个元素即将发生变化的CSS属性。它可以被应用到任何元素上,用于提前告知浏览器该元素将要有哪些属性进行改变,从而优化渲染性能。
通过在元素上设置will-change
属性,开发者可以明确指示浏览器对该元素进行优化处理。这样一来,浏览器可以提前分配资源和准备工作,以便在实际改变发生之前进行相应的合成操作。这样做有助于避免不必要的重绘和重排,提高页面的响应速度和动画的流畅度。
will-change
属性可以接受多个属性值,表示将要改变的属性。例如,will-change: transform
表示元素即将进行变形操作,will-change: opacity
表示元素的透明度即将发生变化。
需要注意的是,will-change
属性应在实际变化发生前的一段时间内被设置,以便浏览器有足够的时间进行准备和优化。另外,will-change
属性并不会自动触发硬件加速,但它可以为浏览器提供一种优化渲染的提示。
此外,will-change
属性的使用应谨慎,避免滥用。只有在明确知道元素即将发生某种变化,并且这种变化对性能有影响时,才应使用will-change
属性。过度使用will-change
属性可能会导致浏览器进行不必要的优化,反而降低性能。
总的来说,will-change
属性是一个用于优化渲染性能的CSS属性,通过提前告知浏览器元素即将发生的变化,使浏览器能够提前进行准备和优化,从而提高页面的响应速度和动画的流畅度
Demo:
css
代码解读
复制代码
div {
will-change: transform;
width: 200px;
background-color: red;
margin: 0 auto;
}
通过上面的 css 设置,告诉浏览器:这个 div 的 transform 经常会变,浏览器会为该元素创建一个独立的图层,将这个图层标记为“即将变换”。这样,在进行布局和绘制时,浏览器就可以更高效地处理这个元素,而无需重新计算整个渲染树。
五、绘制 - Paint
第四步的产出就是分层。第五步绘制阶段,会分别对每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来(类似 canvas 代码,告诉计算机从哪个点经过中间几个点,最后到哪个点绘制一条线,中间内容用什么颜色填充)
渲染主线程的工作到此为止,剩余步骤交给其他线程完成。
六、分块 - Tiling
完成绘制之后,主线程将每个图层的绘制信息提交给合成线程,剩余的工作将由合成线程完成。
合成线程首先对每个图层分块(Tiling),将其划分为更多的小区域。合成线程类似一个任务调度者,它会从线程池中拿出多个线程来完成分块的工作。
QA:浏览器渲染过程中分块的作用是什么?
- 提高渲染性能:当浏览器需要渲染一个复杂的网页时,如果一次性将所有内容加载并渲染出来,可能会消耗大量的计算资源和时间,通过网页分成多个较小的块(tiling)浏览器可以并行地加载和渲染这些块,从而充分利用计算资源,提高渲染性能。
- 优化内存占用:如果浏览器一次性加载和渲染整个网页,可能会消耗大量的内存。通过将网页分成多个图块,浏览器可以更好的管理内存,避免不必要的内存消耗
- 实现懒加载:通过分块,浏览器可以实现懒加载,即只有当某个图块进入视口可见区域时,才开始加载和渲染该图块。这样可以减少不必要的加载和渲染,提高页面的加载速度和性能
- 方便进行并行处理和异步操作:通过将网页分成多个图块,浏览器可以更容易地进行并行处理和异步操作。例如,在移动端设备上,可以利用 GPU 进行图块的并行渲染,提高渲染效率。
分块 tiling 技术是浏览器渲染过程中提高性能、优化内存使用和实现懒加载的重要手段之一。
七、光栅化
分块完成后会进入光栅化阶段。光栅化是将每个块变成位图。 合成线程将分块后的块信息交给 GPU 进程,以极高的速度完成光栅化,并优先处理靠近视口的块。
八、画 - Draw
合成线程计算出每个位图在屏幕上的位置,交给 GPU 进行最终的呈现。
合成线程拿到每个层(Layer)、每个块(Tile)的位图(bitmap)后,生成一个个指引(quad)信息。指引信息标识出每个位图应该画到屏幕上的哪个位置,此过程会考虑到旋转、缩放、变形等。
因为变形发生在合成线程,与渲染主线程无关,这也是 transform 效率高的本质原因。 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕显示
完成过程如下
reflow
比如某个业务逻辑,导致我们用 js 脚本修改了某个节点的宽度、颜色,这其实修改的是 cssom,假设业务逻辑导致操作了 DOM,DOM 节点增删了,cssom、DOM 改变了,则会触发一系列的的流程,比如重新计算样式 style、布局(layout)、分层 layer、绘制 paint、分块 tiling、光栅化 raster、画 draw、系统调用 GPU 去真正显示。
当渲染树 render tree 中的一部分(或者全部)因为元素的尺寸、布局、颜色、隐藏等改变而需要重新构建,这就情况被称为回流。图上的 style 这部分。
回流发生时,浏览器会让渲染树中受到影响的部分失效,并重新构建这部分渲染树。完成回流 reflow 后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
简单来说,reflow 就是计算元素在屏幕上确切的位置和大小并重新绘制。回流的代价远大于重绘。回流必定重绘,重绘不一定回流。
repaint
当渲染树 render tree 中的一些元素需要更新样式,但这些样式只是改变元素外观、风格,而不影响布局(位置、大小),则叫重绘。 简单来说,重绘就是将渲染树节点转换为屏幕上的实际像素,不涉及重新布局阶段的位置与大小计算
QA:为什么 transform 效率高? 假设在 css 中针对某个元素写了 transform: rotate(100deg)
,transform 既不会影响布局也不会影响绘制指令,它影响的是渲染流程的最后一个阶段,图上的 draw 阶段。由于 draw 阶段发生在合成线程中,不在渲染主线程,所以 transform 的任何变化几乎不会影响主线程。同样,不管主线程如何繁忙,都不影响合成线程中 transform 的变化。