浏览器拿到 html 到渲染成页面发生了什么?

一、从获得 Html 到页面渲染的全部流程

1.1 DOM 树构建

渲染器进程接受到的数据也就是 HTML。渲染器进程的核心任务就是把 html、css、js、image 等资源渲染成用户可以交互的 web 页面。渲染器进程的主线程将 html 进行解析,构造 DOM 数据结构。DOM 也就是文档对象模型,是浏览器对页面在其内部的表示形式,是 web 开发程序员可以通过 JS 与之交互的数据结构和 API。html 首先通过 tokeniser 标记化,通过词法分析将输入的 html 内容解析成多个标记,根据识别后的标记进行 DOM 树构造,在 DOM 树构建过程中会创建 document 对象,然后以 document 的为根节点的 DOM 树,不断进行修改,向其中添加各种元素。

1.2 渲染阻塞

html 代码中往往会引入一些额外的资源,比如图片、CSS、JS 脚本等,图片和 CSS 这些资源需要通过网络下载或从缓存中直接加载,这些资源不会阻塞 html 的解析,因为他们不会影响 DOM 树的生成,但当 HTML 解析过程中遇到 script 标签,就会停止 html 解析流程,转而去加载解析并且执行 JS。这是因为浏览器并不知道 JS 执行是否会改变当前页面的 HTML 结构,如果 JS 代码里用了 document.write 方法来修改 html,之前的和 html 解析就没有任何意义了,这也就是为什么我们一直说要把 script 标签要放在合适的位置,或者使用 async 或 defer 属性来异步加载执行 JS。

1.3 Layout Tree

在 html 解析完成后,我们就会获得一个 DOM Tree(树),但我们还不知道 DOM Tree 上的每个节点应该长什么样子,主线程需要解析 CSS,并确定每个 DOM 节点的计算样式,即使你没有提供自定义的 CSS 样式,浏览器会有自己默认的样式表,比如 h2 的字体要比 h3 的大。在知道 DOM 结构和每个节点的样式后,我们接下来需要知道每个节点需要放在页面上的哪个位置,也就是节点的坐标以及该节点需要占用多大的区域,这个阶段被称为 layout 布局,主线程通过遍历 dom 和计算好的样式来生成 Layout Tree。Layout Tree 上的每个节点都记录了 x、y 坐标和边框尺寸。这需要注意的是 DOM Tree 和 Layout Tree 并不是一一对应的,设置了 display:none 的节点不会出现在 Layout Tree 上,而在 before 伪类中添加了 content 值的元素 content 中的内容会出现在 Layout Tree 上,不会出现在 DOM 树里,这是因为 DOM 是通过 HTML 解析获得的,并不关系样式,而 Layout Tree 是根据 DOM 和计算好的样式来生成,Layout Tree 是和最后展示在屏幕上节点是对应的。

1.4 绘制(paint)

现在我们已经知道了元素的大小形状和位置,但还不知道以什么样的顺序绘制(paint)这个节点,例如 z-index 这个属性会影响节点绘制的层级关系,如果按照 dom 的层级结构来绘制页面则会导致错误的渲染。所以为了保证在屏幕上展示正确的层级,主线程遍历 Layout Tree 创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段配称为绘制(Paint)。

1.5 栅格化

现在知道了文档的绘制顺序,终于到了该把这些信息转化成像素点显示在屏幕上了,这个行为被称为栅格化(Rastering)。chrome 最早使用了一种很简单的方式,只栅格化用户可视区域的内容,当用户滚动页面时,再栅格化更多的内容来填充缺失的部分,这种方式带来的问题就是会导致展示延迟。现在 chrome 进行了优化升级,使用了一种更为复杂的栅格化流程叫做合成(compositing),合成是一种将页面各个部分分成多个图层,分别对其进行栅格化,并在合成器线程(compositor Thread)中单独进行合成页面,简单来说就是页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视区的内容组合成一帧展示给用户即可。

1.6 layer tree

主线程遍历 Layout Tree 生成 layer tree,当 Layer Tree 生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块(tiles),然后将每个图块发送给栅格化线程(Raster Thread),栅格化线程栅格化每个图块,并将他们存储在 GPU 内存中,当图块栅格化完成后,合成器线程将收集成为 draw quads 的图块信息,这些信息里记录了图块在内存中的位置和在页面的那个位置绘制图块的信息,根据这些信息合成器线程生成一个合成器帧(Compositor Frame)然后合成器 Frame(帧)通过 IPC 传递给浏览器进程,接着浏览器进程将合成器帧传送到 GPU,然后 GPU 渲染展示到屏幕上。
当页面发生变化时,比如滚动了当前的页面,都会生成一个新的合成器帧,新的帧再传给 GPU,然后再次渲染到屏幕上。

二、为什么栅格线程使用 GPU 计算而不是 CPU 计算?

CPU 与 GPU:CPU 和 GPU 作为计算机中最重要的两个计算单元直接决定了计算性能。CPU是计算机的大脑,负责处理各种不同的任务。在过去,大多数 CPU 是单芯片的,核心被安置在同一个芯片上。更新的 CPU 可以支持多核心,运算能力大大加强。而最新的的 cpu 已经达到 10 核心 20 线程数的能力了。
GPU是另一个计算机的组成部分,与 CPU 不同,GPU 更擅长利用多核心同时处理单一的任务。像命名那样,GPU 最初被用于处理图像。这就是为什么使用 GPU 可以更快、更顺畅的渲染页面内容。随着 GPU 的发展,越来越多的计算任务也可以使用 GPU 来处理。甚至有人说 GPU 是人工智能的大功臣,可见 GPU 已经不再仅用于图像处理上了。