从 chromium 源码来窥探浏览器的渲染

你好,我是承和。

今天给大家分享一下我对于浏览器渲染以及优化方面的一些理解。

传统面试题

我们在各种面试题以及面试中都大概率看到过这个题目,浏览器在拿到数据后到最终呈现在⻚面上经历了哪些过程?

b56b4e57707e190c4e79d68f0dd48071.png

这绝不是浏览器“刷”的一下就把页面给渲染出来了,中间经历了非常复杂的流程,我们一点点由浅入深来窥探浏览器渲染的整个过程。

传统回答

首先我们可以看下比较传统的回答方式,我们不去考虑 js 文件对⻚面解析造成的影响,在最简单的⻚面中,浏览器仅仅拿到 HTML 文件CSS 文件 ,便可以渲染出一个⻚面。在浏览器引擎中,会分别使用 HTML 解析器以及 CSS 解析器将接收到的二进制流数据转化为浏览器能够识别的 DOM 树和 CSS 规则树,随后将两者进行结合生成 Render(Layout Object)树,浏览器在拿到 (Layout Object)树后再经历分层,绘制等,我们才能在屏幕上看到最终的⻚面。

Chrome 多进程机制

我们从另一个⻆度,Chrome 多进程的⻆度也可以进行探索,大家都知道 Chrome 采用的是一个 多进程架构。 详情参考现代浏览器架构。例如浏览器进程,负责浏览器主框架,提供一些通用的能力,GPU 进程则负责将渲染进程上传到 GPU 中的位图纹理进行处理随后呈现到屏幕上等。而浏览器渲染关联的渲染进程,当然是我们最关心的,它究竟是由哪些线程组成的,以及各个线程之间是如何通信合作来完成渲染的呢?

渲染进程组成

渲染进程主要由如下几个线程组成:

  1. GUI 渲染主线程: 解析 html,css,构建 DOM 树和 LayoutObject。

  2. JS 引擎线程: 执行,解析 JS 代码。

  3. 合成器线程:进行分块操作,同时也负责接受用戶的滚动,输入,分发回调事件等。

  4. 栅格化线程:将绘制命令转换为位图或者 GPU 能识别的纹理。

我们可以通过 chrome://tracing 记录在一个⻚面渲染过程中,各个线程之间的通信:

2fd1ece71236f9c14e200af6f5dcafd2.png

如上所示:CRFRenderMain 表示的是渲染主线程,主要进行一些计算的操作。Compositior 表示合成器线程,主要进行合成操作。Compositior Tile Workder 表示栅格化线程,现代浏览器往往有 2-4 个栅格化线程,浏览器会根据资源情况合理分配栅格化线程资源。

线程间通信过程

我们从一帧渲染开始,来看各个线程之间的通信过程,如下所示:

032e9dfa2d50945eea84caacc57e17eb.png

同时Blink内核为了清晰区分各个阶段,也定义了一个类DocumentLifeCycle 来确保各个阶段不会发生来回的跳转。类似于 React 当中的生命周期,一帧的渲染是一个 原子操作, 只要开始渲染便会一路执行到底,而不会进行回滚操作。

425f524f42c6c9c37dd1d389d68a1b18.png

整体的渲染流程如下:

  1. 合成器线程接收到Vsync信号,开始新的一帧绘制。

  2. 我们知道合成器线程可以处理用戶的输入,如果一些输入事件中存在一些回调事件,例如滚动的回调事件,那么合成器线程在上一帧收集完这些事件之后,会在当前帧将这些事件交给渲染主线程进行处理。

  3. 执行 requestAnimationFrame 相关的动画操作。

  4. 解析 HTML 数据,形成 DOM 树,这是 HTML 解析器(HTMLParser)的主要工作。浏览器接收到的html 数据也是字节流,因此要将其转换成浏览器能认识及转换的 token 标签,在这之中主要经历了如下步骤:

  • 4.1. 解码:浏览器将接收的字节流(Bytes)基于编码方式解析为字符(characters)。

  • 4.2. 分词:通过分词器(词法分析)将字符转换为 Token,分为 Tag Token 和文本 Token。详情可参考 vue 源码中的模板解析过程,大部分还是相同的。

  • 4.3. 将 tokens 标签转换为 nodes 节点,随后将 nodes 节点添加至 DOM 树上,这两步是并行执行的,在这期间,主要是通过的数据结构来进行维护(类似于常⻅面试题-括号匹配),当遇到开标签时,将对应 node 推入栈中,并且添加至 DOM 树上,当遇到文本标签时,就直接将文本 node 添加至 DOM 树上即可,当遇到闭合标签时,就进行出栈操作。另外 html 是一⻔友好语言,对于开闭标签不匹配的场景,或者是自定义的标签,都有自己的处理方式,在这里就不做具体展开。

df61a2b33c6f899bc8d36cc91b77c2ca.png
  1. 在有了 DOM 树之后,就需要去计算样式,计算样式的主要过程就不展开了,我们主要来看 CSS 解析器的产物,便是 styleSheets,在控制台使用document.styleSheets可以看到:

87272b6c5c026b04a0f09b6495d00046.png

关于 stylesheets 的具体属性,可参考stylesheets 详解

我们引入 css 的方式主要有行内样式,行内样式表,外部样式表(最经常使用),这里的 stylesheets 便是一个个引入方式的最终解析产物。

在将各个引入方式进行解析后,我们就要将这些样式赋予我们的 DOM 节点,浏览器会结合 CSS 的 继承优先级层叠 等规则,形成 CSS 规则树,可以通过浏览器的Element->Computed 查看一个DOM 节点上的具体样式。

218a81e3a6d0182c3b847c1ccd1d5615.png
  1. Layout,计算布局,这里主要是将⻚面中真正需要渲染的元素在 Layout Object 树中进行展示。

  2. 更新 Layer Tree,这里主要是进行一些分层的操作,例如,更新 Paint Layer Tree 及 Graphic Layer Tree,在下文会进行具体展开。

  3. Paint,生成绘画指令,以及记录需要执行哪些绘画调用和调用顺序,将其序列化记录进 SkPicture 数据结构中。

  4. Composite,计算出每个合成层在合成时所需要的数据,包括位移(Translation)、缩放(Scale)、旋转(Rotation)混合等操作的参数。

以上这些都主要是在浏览器主线程中进行的操作,可看出主要进行的都是些复杂度不是很高的计算操作,而 JS 的解析执行则会放到专⻔的 JS 解析线程中去执行。

92eecaabacdfefc9b47bd2b52178acb9.png
  1. 提交至合成器线程,合成器线程主要在做的就是一个分块操作,大家都知道,若我们对一个⻚面中的所有元素进行绘制的话是非常消耗性能的(因为可视区域外的渲染根本没有必要),因此我们会首先进行分块操作,将⻚面中的可视元素进行摘取并绘制。这样可以大大地节省资源。

  2. 栅格化,栅格化主要进行的操作便是将上面产生的绘制指令转换成 GPU 能识别的位图或者纹理,主要有以下 2 种方式。

  • a. 基于 CPU,使用 Skia 库的软件加速(Software Rasterization),首先绘制进位图里,然后再作为纹理上传到 GPU。

  • b. 基于 GPU,采用硬件加速(Hardware Rasterization),这个过程是借助 OpenGL 直接在 GPU 纹理中进行绘制和光栅化,填充像素,也就是 GPU Raster。

  1. 提交至 GPU 进程,进行渲染和前后缓冲区的交换,将结果展示至屏幕中。

分层阶段

在上面阐述的线程间通信中,我们主要忽略了分层这个过程,下面我们就主要来研究分层过程中产生的 3 棵树究竟是用来干嘛的。

00743e6da1c2eacaf8d6b8d06eb73754.png

Layout Object Tree (布局树)

作用:DOM 节点可以分为可视化节点(div,p),非可视化节点(script,meta,head)等。Render 树的作用就是展现⻚面上真正需要渲染的元素,忽略掉不可⻅元素,例如(display:none),添加不存在 DOM 树中但需要显示的内容(例如伪元素)。

布局树形成概览
10dd41ba74e7c31d59a68e150776d25b.png

产生的过程如上图所示,便是将 DOM 树和 CSS 规则树进行结合,忽略掉不可⻅元素,以及添加可⻅元素,来计算出各个元素在⻚面中的位置。

在 Chrominum 中的源码也较简单,通过判断 display 来产生不同的 Layout Object。

0e032a405a4fc1beee0ec12efbf281e9.png
映射关系

各个 html 节点与 Layout Object 的映射关系如下所示,他们都继承自同一个基类 LayoutObject,在其 基础上衍生出了不同的盒子模型,例如我们熟知的块级元素,行内元素以及行内块元素等,这些不同的类都定义了对其子元素以及兄弟元素之间是如何进行布局的,在这不做具体展开。

bd838235d261f94d68d4da41b893885e.png
例子

我们也可以拿一个最简单的布局代码举例,来看看他会产生怎样的一颗 Render 树。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <p>123</p>
    </div>
    <div>
      1
      <p>456</p>
    </div>
  </body>
</html>
Content Shell

我们利用的是 Chromium 官方提供的 Content Shell 命令行工具,该工具拥有 Chrome 内核,但是没有 UI,详情参考,运行如下命令后,便可以看到上述代码产生的 Render 树。

out/mychromium/Content\ Shell.app/Contents/MacOS/Content\ Shell --run-web-tests ~Desktop/层演示/Layout/index.html
4308131049f4786a02ff331edfa1fb03.png这里要注意的一点便是文本1外部了一个匿名块元素,因为行内元素不能和块级元素相邻,所以为了布局方便,产生了一个匿名块元素包裹在文本元素外围。

Paint Layer(渲染层)

一般来说,在 Render 树的基础上,我们会将拥有相同z 坐标空间的 Layout Objects,归属到同一个渲染层(Paint Layer)中。Paint Layer 最初是用来实现stacking context(层叠上下文),类似于画一张蓝天白云图,我们要确定究竟是先画蓝天,还是白云。若先画白云,再画蓝天,会出现白云不可⻅的错误。层叠上下文的作用亦是如此,它主要来保证⻚面元素以正确的顺序合成。

渲染层分类

渲染层也可以主要分为以下 3 类,各个渲染层的主要形成原因如下所示:

  1. kNormalPaintLayer

  • 根元素(HTML)

  • position 值为 absolute 或 relative,且 z-index 不为 auto 的元素

  • position 值为 fixed 或 sticky 的元素

  • flex 容器的子元素,且 z-index 值不为 auto

  • grid 容器的子元素,且 z-index 值不为 auto

  • mix-blend-mode 属性值不为 normal 的元素

  • 以下任意属性值不为 none 的元素:

    • transform

    • filter

    • perspective

    • clip-path

    • mask/mask-image/mask-border

    • isolation 属性值为 isolate 的元素

  1. kOverflowClipPaintLayer

  • overflow 不为 visible

  1. KNoPaintLayer

  • 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div

举例
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div style="position: absolute; background-color: yellow;z-index:1">
      <div style="opacity: 0.1">opacity</div>
      <div style="filter: blur(5px)">filter</div>
      <div style="transform: translateX(20px)">tranform</div>
      <div style="mix-blend-mode: multiply">mix-blend-mode</div>
      <div style="overflow: hidden">overflow</div>
    </div>
  </body>
</html>

我们也可以利用 Content Shell 查看上述代码产生的,Paint Layer 分层情况。

out/mychromium/Content\ Shell.app/Contents/MacOS/Content\ Shell --run-web-tests ~Desktop/层演示/Paint/index.html
e057618a475f048a4cb8d9e9d34e4fb4.png

可看到为各个满足生成渲染层条件的 html 元素都形成了一个渲染层。

整体流程

整体源码中生成渲染层的调用流程如下所示:

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值