Skip to content

深入理解浏览器的渲染流程

本文完全参考了 浏览器工作原理与实践 ,并摘抄了一些关键部分,大家共同学习!

线程 VS 进程

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程

20230131144432

从图中可以看到,线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

进程和线程之间的关系有以下4个特点:

  1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
  2. 线程之间共享进程中的数据。
  3. 当一个进程关闭之后,操作系统会回收进程所占用的内存
  4. 进程之间的内容相互隔离。

由于进程的隔离性,一个tab页面就是一个单独的进程,这样就能保证一个页面崩溃不会影响其他页面

浏览器进程

  • 浏览器主进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

不可避免地带来了一些问题: 更高的资源占用。因为每个进程都会包含公共基础结构的副本(如JavaScript运行环境),这就意味着浏览器会消耗更多的内存资源

渲染流程:HTML、CSS和JavaScript是如何变成页面的

渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线,其大致流程如下图所示:

20230131150642

流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

1. 构建DOM树

浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树。

20230131151041

生成的dom树会保存在内存中,可以通过JS获取到dom节点。

2. 样式计算

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大体可分为三步来完成

2.1 把CSS转换为浏览器能够理解的结构

CSS样式来源主要有三种:

  • 通过link引用的外部CSS文件

  • <style>标记内的 CSS

  • 元素的style属性内嵌的CSS

  • 和HTML文件一样,浏览器也是无法直接理解这些纯文本的CSS样式,所以当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。

    只需要在控制台中输入document.styleSheets,然后就看到如下图所示的结构

20230131151328

2.2 转换样式表中的属性值,使其标准化

20230131151454

2.3 计算出DOM树中每个节点的具体样式

20230131151601

从图中可以看出,所有子节点都继承了父节点样式。比如body节点的font-size属性是20,那body节点下面的所有节点的font-

3. 布局阶段

3.1 创建布局树

20230131151833 你可能注意到了DOM树还含有很多不可见的元素,比如head标签,还有使用了display:none属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。

通过 StyleSheets和DOM树,最后生成了布局树

为了构建布局树,浏览器大体上完成了下面这些工作

  • 遍历DOM树中的所有可见节点,并把这些节点加到布局中;
  • 而不可见的节点会被布局树忽略掉,如head标签下面的全部内容,再比如body.p.span这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树

3.2 布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。

4. 分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?

答案依然是否定的。

因为页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。

下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:

20230131152221

那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?

  • 拥有层叠上下文属性的元素会被提升为单独的一层。 20230131155151

    从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。

  • 需要剪裁(clip)的地方也会被创建为图层。

5.图层绘制

试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?

通常,你会把你的绘制操作分解为三步

  • 制蓝色背景;
  • 在中间绘制一个红色的圆;
  • 再在圆上绘制绿色三角形

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表

绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

6. 栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。

20230131162508

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

20230131162632

通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。

相信你还记得,GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。

20230131162657

7. 合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了。

总体流程

结合上图,一个完整的渲染流程大致可总结为如下

  • 渲染进程将HTML内容转换为能够读懂的DOM树结构。
  • 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  • 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上

重排

如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局。会重新执行布局和分层等。

20230131163440

重绘

如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

20230131163502

使用transform来控制动画

CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

20230131163558

参考