阅读 67

深入浏览器渲染原理

序言

作为一名前端工程师,学习和掌握浏览器内部工作原理不仅可以让我们的页面内容快速加载,还可以让我们理解那些最佳开发实践的个中缘由,同时也是我们做性能优化的必备知识。根据 StatCounter浏览器统计数据,chrome浏览器的市场占有率已经64%,因此,这篇文章主要以chrome为例来探究其渲染原理。

进程和线程

在理解浏览器架构之前我们先补充下操作系统的相关知识,以便于我们更深入的了解。

  • 多任务:现代操作系统比如Mac OS,Linux,Windows等都是支持“多任务”的操作系统。简单来说就是,你可以一边打开网易云音乐听歌,一边用chrome上网,一边用vscode写代码,这个就是多任务。
  • 进程:为了更好的描述和控制多任务,实现操作系统的并发性和共享性,为此引入了进程的概念。对于操作系统来说,一个任务就是一个进程(process),打开一个浏览器就是启动了一个浏览器进程,打开一个记事本就是启动了一个记事本进程。进程是计算机分配资源的最小单元。计算机的资源分为计算资源和存储资源。因此,进程是对计算机程序的抽象:
1. 进程表示一个逻辑控制流,就是一种计算过程,它造成一个假象,好像这个进程一直在独占CPU资源
2. 进程拥有一个独立的虚拟内存地址空间,它造成一个假象,好像这个进程一致在独占存储器资源
复制代码
  • 线程:为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能,引入了线程的概念。因此,可以理解为在一个进程内部,需要同时干多件事,就需要同时运行多个子任务,这个子任务就可以理解为线程。

浏览器架构(Chrome)

到底我们打开一个页面chrome需要启动多少进程呢?我们在chrome中随意输入一个网址,然后代开chrome浏览器右上角的工具栏,然后选择“更多工具”,点击任务管理器,你会看到如图所示: WX20210416-004902.png
可以看到有浏览器进程,GPU进程,NetworkService进程,StorageService进程,AudioService进程,还有相关的标签页进程等。最新的chrome进程架构如下图所示:

5bee5bceb417a.png 从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。 下面我们对这个几个进程进行说明和分析:

  1. 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储功能。
  2. 渲染进程:主要是将HTML、CSS和JavaScript转换为用户可以交互的网页。默认情况下Chrome会为每个Tab标签页创建一个渲染进程。出于安全考虑,渲染进程都运行在沙箱环境中。
  3. GPU进程:主要完成3Dcss的效果,负责UI界面的绘制。
  4. 网络进程:负责网页网络资源的加载包括css,html,img,js等。
  5. 插件进程:主要负责chrome的一些扩展功能的运行,对于前端来说比较熟悉的就是Vue devtool和React devtool。

那么这几个进程他们之间是怎么进行协作的呢?当我们从输入url到页面展示其流程如下图所示:

92d73c75308e50d5c06ad44612bcb45d.png 从图中我们可以看到各个进程间的配合。

渲染流程

前面我们分析了chrome的架构,我们对于chrome浏览器有了一个大的认识。那么对于html、css和javascript是如何变成页面的呢?我们就来聊聊渲染流程,同时也是渲染进程如何工作的。
渲染进程包含一下线程:

  1. 主线程(main thread)
  2. 工作线程(worker thread)
  3. 合成线程(compositor thread)
  4. 光栅线程(raster thread)

构建DOM树

渲染进程在接收到浏览器进程的提交导航(commit navigator)的消息后,渲染进程会接收HTML数据,并且把它转换为一个DOM(Document Object Model)对象.其转换过程如下图所示:

86935079-07866400-c16f-11ea-86bd-98e83357a96a.jpeg

  1. 转换:浏览器从读取HTML的原始字节,并根据文件的指定编码将字节转换为字符。
  2. 令牌化:浏览器将字符根据HTML中的定义,转换为token,序列化后的token如图所示:

WX20210417-114505.png
3. 一个token就是一个标签文本的序列化,并将这些token分类处理,转换成不同的node节点对象
4. 对不同的标签创建不同的html元素,并建立起他们的父子兄弟关系,最终构成一个DOM树。

样式计算

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

1.格式化样式表

和HTML一样浏览器是无法直接理解纯文本的CSS样式,所以渲染引擎接收到CSS文本时,会执行转换操作,将其转换为stylesheets。你可以在浏览器控制台通过document.styleSheets拿到。

2.标准化样式表

我们来看这段代码:

    body { font-size: 2em }
    p {color:blue;}
    span {display: none}
    div {font-weight: bold}
    div p {color:green;}
    div {color:red; }
复制代码

其转化后如下图所示:

1252c6d3c1a51714606daa6bdad3a560.png 我们可以看到他会把em为单位的转换为px,会把red值转换为rgba

3.计算每个DOM节点的具体样式

计算DOM节点的规则主要是:继承和层叠

- 继承: 每个自节点会默认去继承父节点的样式,如果父节点中找不到,就会采用浏览器的默认样式,也叫UserAgent样式  
- 层叠:样式层叠是css的一个基本特性,它定义如何合并来自多个源的属性的算法。css全称层叠样式表正是强调了这一点。  
复制代码

转换后的style如下图所示:

WX20210417-133041.png 这个阶段完成了对DOM节点的具体计算,所有的样式值会挂载window.getComputedStyle中。你可以通过js方便的获取到。

布局阶段

现在,我们有DOM树以及DOM树中每个元素的样式,但是这还不足以显示,因为我们还不知道DOM的几何位置。接下来就是通过浏览器的布局系统确定元元素位置,我们把这个过程叫做布局。
构建布局树主要是下面两步:

  1. 遍历DOM树可见节点,并把这些节点加到布局树中
  2. 对于不可见的节点,head,meta标签都会被忽略。对于body.p.span这个元素其属性为display:none,因此不会被包含进布局树中

具体过程如下图所示:

86935715-b9259500-c16f-11ea-9320-ae897ba64139.png

分层

知道了DOM节,及DOM节点的样式,还把这些样式通过计算的到布局树,那么有了布局中我们就知道了具体元素的位置信息,我们是不是就可以绘制页面了?

答案是还不行。

因为页面中还有很多其他复杂的特效:例如z-index,3D变换,我们虽然知道了画布上每个元素的大小,位置信息,但是不知道每个元素的绘画顺序,因此,渲染引擎还需要为特殊的节点生成图层,并构成一颗图层树。你可以通过chrome工具栏里面的more tools选择layers可以看到。布局树与图层之间的关系如图所示:

86935766-c80c4780-c16f-11ea-9f89-0703a0024096.png
浏览器的页面实际被分成了很多图层,这些图层叠加后合成了最终的页面

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就从属于父节点的图层。

那么什么样的情况下,渲染引擎会为特定的节点创建新的图层呢?

拥有层叠上下文的元素

一般有一下特定的css属性:
1.HTML根元素本身就具有层叠上下文
2.普通元素设置了position属性不为static,并且设置了z-index属性,会产生层叠上下文
3.元素的opacity属性不为1
4.元素的transform值不为none
5.元素的filter值不是none
6.元素的isolation值是isolate
7.will-change

更多层叠上下文知识,可以参考这篇文章

需要剪裁(clip)的地方

比如一个标签很小,50*50像素,你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条也会被单独提升为一个图层

合成

到目前为止,浏览器已经知道了关于页面以下的信息:文档结构,元素的样式,元素的几何信息以及它们的绘画顺序。那么浏览器是如何利用这些信息来绘制出页面来的呢?将以上这些信息转化为显示器的像素的过程叫做光栅化(rasterizing)。

合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。

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

40825a55214a7990bba6b9bec6e54108.png
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:

46d33b6e5fca889ecbfab4516c80a441.png

有的图层很大,你可能需要滚动好久才能到底部,所以合成线程会把每个图层拆分成一小块一小块的图块,然后将图块发送给光栅线程,光栅线程会栅格化每个图块并且把它们存储在GPU的内存中。

因此,光栅化通俗来说就是将图块转换为位图的过程。

合成线程可以给不同的光栅线程赋予不同的优先级(prioritize),进而使那些在视口中的或者视口附近的页面可以先被光栅化。

a8d954cd8e4722ee03d14afaa14c3987.png 一旦所有图块都被光栅化,当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)

  • 绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息。
  • 合成帧:代表页面一个帧的内容的绘制四边形集合。

上面步骤完成后,合成线程就会通过ipc向浏览器进程提交渲染帧,这些合成帧都会被发给GPU渲染到屏幕上。

优化

到这里我们分析完了整个渲染流程,整个过程如图所示:

975fcbf7f83cc20d216f3d68a85d0f37.png
其实渲染过程有叫做渲染流水线。

关于渲染流水线有一个十分重要的点就是流水线的每一步都要使用到前一步的结果来生成新的数据,这就意味着如果某一步的内容发生了改变的话,这一步后面所有的步骤都要被重新执行以生成新的记录。

为此,我们需要了解重绘和重排的相关概念

更改了元素的几何属性(重排)

b3ed565230fe4f5c1886304a8ff754e5.png
从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

这些css属性会触发布局:

  • width height
  • padding margin
  • display border-width
  • border top
  • position font-size
  • float text-align
  • overflow-y font-weight
  • overflow left
  • font-family line-height
  • vertical-align right
  • clear white-space
  • bottom min-height

一些常用且会导致回流的属性和方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()
更新元素的绘制属性(重绘)

3c1b7310648cccbf6aa4a42ad0202b03.png

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

这些属性会触发重绘

  • color border-style
  • visibility background
  • text-decoration background-image
  • background-position background-repeat
  • outline-color outline
  • outline-style border-radius
  • outline-width box-shadow
  • background-size
直接合成阶段

合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及JavaScript完成执行。这也就是为什么说只通过合成来构建页面动画是构建流畅用户体验的最佳实践的原因了。如果页面需要被重新布局或者绘制的话,主线程一定会参与进来的。

024bf6c83b8146d267f476555d953a2c.png

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

参考

浏览器工作原理与实践

一文看懂Chrome浏览器工作原理

浏览器渲染流程

文章分类
前端
文章标签