阅读 1052

浏览器知识点整理(七)渲染流程

前言

上一篇文章 已经详细介绍了浏览器的 导航流程,即 从用户发出 URL 请求到页面开始解析的过程,那么接下来将详细介绍浏览器的 渲染过程,即 从页面开始解析到页面完整展示的过程

我们把 HTML、CSS、JavaScript 等文件交给浏览器,经过浏览器内部的一些处理就可以显示出漂亮的页面,那在探究浏览器内部怎么处理之前,先回顾一下 HTML、CSS、JavaScript 是什么:

  • HTML:HTML 的内容是 由标签和文本组成。每个标签都有它自己的语义,浏览器会根据标签的语义来正确展示 HTML 内容。
  • CSS即层叠样式表,由选择器和属性组成,如果需要改变 HTML 的字体颜色、大小等信息,就会用到 CSS。
  • JavaScript:简称 JS,通过 JS 可以修改页面内容,让页面“动”起来。

这也是 Web 标准的三大组成部分:结构、表现和行为

浏览器把 HTML、CSS、JS 渲染成页面是很复杂的一个过程,所以在渲染过程中会划分为多个子阶段,按照时间顺序可以划分为如下几个阶段:构建 DOM 树样式计算(解析 CSS)、获取布局树生成图层树图层绘制栅格化处理合成显示

下面将逐一介绍这些子阶段,每个阶段都有 获取输入内容处理内容生成输出内容 这样的周期存在,关注这些内容会更加清晰地了解每个子阶段。

构建 DOM 树

浏览器是没有办法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。DOM 把文档作为一个树形结构,树的每个结点表示了一个 HTML 标签或标签内的文本项

DOM 提供了对 HTML 文档结构化的表述。在渲染引擎中,DOM 有三个层面的作用:

  • 从页面的视角来看,DOM 是生成页面的基础数据结构
  • 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
  • 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容会在 DOM 解析阶段被拒之门外。

简而言之,DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容

构建 DOM 树的过程:

  • 输入:从网络请求或者缓存中拿到的 HTML 文件
  • 处理:通过 HTML 解析器解析生成 DOM 树,大概过程就是:拿到 HTML 文件的字节流数据,将这些字节数据 转化为字符串,然后将这些字符串转化为 标记token),然后把这些标记进行 词法分析 转化为 DOM 节点,再将 DOM 节点 构建为一棵 DOM 树
  • 输出输出一个树状结构的 DOM,即 DOM 树

DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容

如下,我们可以通过打印 document 或在 Elements 查看 DOM,下面是一个简单的 DOM 树示例:

image.png

样式计算

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式

和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以渲染引擎会先将其解析成 浏览器可以理解的结构,这个结构就是 styleSheets。网上有很多地方是说把 CSS 文件转换为 CSSOM 树的,而在 Chromium 源码(需要翻墙) 上并没有 CSSOM 这个词,这个 styleSheets 是能够直观感受得到的。不过名词是什么并不重要,我也喜欢 CSSOM 这个词,重要的是我们要理解这个 样式计算 的过程。

我们可以通过 document.styleSheets 查看 styleSheets 结构:

image.png

样式计算的过程:

  • 输入:CSS 样式来源主要有三种:
    • 通过 link 引用的外部 CSS 文件
    • <style> 标签内的 CSS
    • 元素的 style 属性内嵌的 CSS
  • 处理
    • 把 CSS 转换为浏览器能够理解的结构-- styleSheetsstyleSheets 具有两个作用:
      • 第一个是 提供给 JavaScript 操作样式表的能力
      • 第二个是 为布局树的合成提供基础的样式信息
    • 转换样式表中的属性值,使其标准化;即需要把所有的值转换为渲染引擎容易理解的、标准化的计算值
      • 例如:把 2em 转换为 32px、把 blue 转换为 rgb(0, 0, 255)、把 font-weight: bold 转换为 font-weight: 700
    • 计算出 DOM 树中每个节点的具体样式,在这个过程中需要遵守 CSS 的继承规则和层叠规则
      • CSS 继承就是每个 DOM 节点都包含有父节点的样式
      • 层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。
  • 输出每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构

可以按 F12 打开开发者工具随便选择一个 Elements 元素,再选择 Computed 标签,如下图,右下角红色框内便是选中标签最终的 ComputedStyle 的值,了解了这个,平时改内置组件的样式会不会方便一些呢~

image.png

获取布局树

现在已经有了 DOM 树和 DOM 树中每个元素的样式,但还不能够渲染一个完整的页面,因为浏览器并不知道这些元素在页面上的 几何位置。所以接下来就是 计算出 DOM 树中可见元素的几何位置,也就是布局(Layout)

布局阶段:

  • 输入:DOM 树和 CSSOM 树
  • 处理
    • 构建一棵只包含可见元素的布局树
      • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中
      • 而不可见的节点会被布局树忽略掉,如属性包含 dispaly:none 的元素不会被包进布局树
    • 布局计算:计算布局树节点的坐标位置,然后写回布局树中
  • 输出:包含 DOM 元素样式和位置的布局树

如下图(来自 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?):

image.png

生成图层树

得到 布局树 之后要对其进行 分层,就好像搞定了它的 xy 轴,之后是处理的是它的 z 轴,在 CSS 上比较能体现的就是 z-index z 轴排序属性、position 定位属性、opacity 透明属性等。通过分层可以实现一些复杂的效果,比如 3D 变换、页面滚动等,那么为了更加方便的实现这些效果,渲染引擎会为特定的节点生成专用的图层,并且生成一棵对应的图层树LayerTree)。如果了解过 Photoshop,相信会更加容易理解图层这个概念。

当然 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。要满足以下两个条件中的一个,渲染引擎才会为特定的节点创建新的图层:

  • 第一点,拥有 层叠上下文属性 的元素会被提升为单独的一层。
    • 即明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等拥有层叠上下文属性的元素
  • 第二点,需要裁剪的地方 也会被创建为图层。
    • 需要裁剪即那些内容超出指定区域的,如一个 div 它的大小为 200 * 200 像素,当里面的文字内容过多超出 200 * 200 的面积时会裁剪,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。

这个图层分层的情况可以打开 Chrome 的开发者工具 Layers 查看,默认展示的工具中是没有的,它在右上角 ... 更多中的 More tools 里面,它有一些如平移、旋转、复位的操作,见下图:

image.png

生成图层树 的过程:

  • 输入:布局树
  • 处理:将特定节点生成专用图层
    • 拥有 层叠上下文属性 的元素会被提升为单独的一层
    • 需要裁剪的地方 会被创建为图层
  • 输出:图层树

图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行 绘制,它会把一个图层的绘制拆分成很多小的 绘制指令,然后再把这些指令按照顺序组成一个待绘制列表

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

如下图,打开 Chrome 的开发者工具中的 Layers 工具,选择 document 层,可以来实际体验下绘制列表,左边的区域是 document 的绘制列表,拖动右边区域中的进度条可以重现列表的绘制过程。

image.png

图层绘制 的过程:

  • 输入:图层树
  • 处理
    • 渲染引擎对图层树中每个图层进行绘制
    • 拆分成绘制指令,生成绘制列表,提交到合成线程
  • 输出:绘制列表

栅格化处理

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

当图层的绘制列表准备好之后,主线程 会把该绘制列表提交给 合成线程。通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做 视口viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块tile),这些图块的大小通常是 256x256 或者 512x512

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

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

栅格化处理 的过程:

  • 输入:绘制列表
  • 处理
    • 根据视口,合成线程会将图层划分为 图块
    • 合成线程会按照视口附近的图块来优先生成 位图
  • 输出:保存在 GPU 内存中的位图

合成显示

  • 一旦所有图块都被栅格化,合成线程 就会生成一个绘制图块的命令——DrawQuad,然后将该命令提交给 浏览器进程
  • 浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

这样到显示阶段,页面就加载显示完成了,这就是整个过程了。

如下图(来自 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?):

image.png

渲染流程总结

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

以上就是 宏观视角 下浏览器的渲染流程了,参考了极客时间李兵老师的 《浏览器工作原理与实践》,这是一个非常棒的专栏,推荐阅读。

浏览器系列专栏目录

文章分类
前端
文章标签