转载请保留这部分内容,注明出处。
另外,头条号前端团队非常 期待你的加入
浏览器是每个前端同学开发时经常接触的工具,甚至可以说是主场。在开发过程中,前端同学大概了解浏览器的渲染流程便可以进行开发,但涉及到页面性能优化、加载速度优化等,就需要比较深入地了解浏览器原理。
关于浏览器渲染流程,很多同学会给出如下答案,也是我之前一直以为的答案:
浏览器根据html文件构建出DOM Tree,根据CSS文件构建出CSS stylesheets,浏览器解析完CSS和JS文件后将两个文件结合生成Render Tree,然后渲染到屏幕上。
这个答案不算错,但是不够具体。本文就来详细地聊聊浏览器渲染,在开始正文之前,需要特别指出, 本文的分析都是基于 Chrome 浏览器。 国内大部分的浏览器内核都是基于 Chromium 二次开发而来,渲染流程大致相同只在产品层面有一些差异。
浏览器进程
在开始说浏览器渲染流程前需要先了解一下浏览器进程,浏览器经过多年发展已经进入多进程浏览器时代,目前 Chrome 浏览器采用的进程架构如下:
从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。那这些进程的功能分别是什么呢?
-
浏览器进程。 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
-
渲染进程。 核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中。默认情况下,Chrome 浏览器会为每个tab标签创建一个渲染进程,但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。
-
GPU进程。 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
-
网路进程。 主要负责页面的网络资源加载。
-
插件进程。 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
基于该模型,在 Chrome 中打开一个页面 至少 需要四个进程,可以通过更多工具-任务管理器查看:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
下面就开始本文的“重头戏”吧,一起来探索从文件到最后呈现的图像中间发生了什么?
第一步:构建 DOM 树
服务器响应浏览器的HTML请求后,浏览器进程便开始准备渲染进程,渲染进程准备好之后,从网络进程获取到具体的HTML文件,通知浏览器进程移除旧的文档,便进入解析阶段。
渲染进程解析HTML文件,构建成DOM树,如下所示:
可以在浏览器控制台输入 document 查看网页生成的 DOM 树。
第二步:样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
2.1 把 CSS 转换为浏览器能够理解的结构
CSS 样式来源主要有三种:
-
通过 link 或者
@import引用的外部 CSS 文件 - 标记内的 CSS
-
元素的 style 属性内嵌的 CSS
浏览器是无法直接理解这些纯文本的 CSS 样式的,所以 当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。 可以打开控制台输入 document.styleSheets 查看其结构。
这里需要特别指出的是,很多文章将CSS文件构建出的文件称为 CSSOM ,源码中并没有这个概念,产物应该是 styleSheets。
2.2 标准化属性值
属性的值一般有多种声明方式,看看下面这段 CSS 文本:
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
其中,渲染引擎无法直接理解 2em 、 blue 、 bold 等值,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
那标准化后的属性值是什么样子呢?
2.3 计算出每个 DOM 节点的具体样式
每个 DOM 节点的样式属性,是 CSS 继承规则和层叠规则 进行叠加计算的结果。
- 每个DOM 节点都会继承其父节点的样式
- 层叠规则
层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点,具体的计算规则此处不做赘述。
经过计算,每个节点都会有一个最终样式,可以通过 Elements -> Computed 查看。
第三步:布局阶段
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。 那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。
3.1 创建布局树
DOM 树中包含很多不可见的元素,所以在显示元素之前,还要额外地构建一棵只包含可见元素布局树。
构建过程大致如下:
-
遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
-
忽略不可见节点,比如
head标签,比如具有display:none样式的元素。
3.2 布局计算
有了布局树之后,就需要对布局树中各节点的几何坐标位置进行计算,输出带有坐标位置的布局树。
第四步:分层
现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,如一些 3D 变换、页面滚动、Z 轴排序等, 为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。
要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示:
其中 Compositing Reasons 指出了该图层被独立的原因。
此时我们已经知道,浏览器中的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。图层和布局树节点之间的关系如下图所示:
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
4.1 加餐:什么样的节点会创建独立的图层?
满足如下任一条件的元素会被提升为单独的一个图层:
4.1.1 拥有层叠上下文属性的元素会被提升为单独的一层
页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。可以结合下图来直观感受下:
从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
文档中的层叠上下文由满足以下任意一个条件的元素形成:
-
根元素 (HTML),
-
z-index 值不为 auto 的绝对/相对定位元素,
-
固定( fixed ) / 沾滞( sticky )定位(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持),
-
z-index 值不为 auto 的 flex ( flexbox )子项 (flex item),即:父元素 display: flex|inline-flex ,
-
opacity 属性值小于 1 的元素(参考 the specification for opacity ),
-
mix-blend-mode 属性值不为 normal 的元素,
-
以下任意属性值不为 none 的元素:
-
isolation 属性被设置为 isolate 的元素,
-
-webkit-overflow-scrolling 属性被设置为 touch 的元素,
-
在 will-change 中指定了任意CSS属性(参考 这篇文章 ),
-
contain 属性值为 layout 、 paint ,或者综合值(比如 contain: strict 、 contain: content )。
在层叠上下文中,其子元素同样也按照上面解释的规则进行层叠。 特别值得一提的是,其子元素的 z-index 值只在父级层叠上下文中有意义。子级层叠上下文被自动视为父级层叠上下文的一个独立单元。关于层叠上下文,更加详细的信息看 这里 。
4.1.2 需要剪裁(clip)的地方也会被创建为图层
当子元素需要展示的区域大于父元素的大小时,就需要进行裁剪。如下例子:
<style>
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
</style>
<body>
<div >
<p>我是子元素一:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长</p>
<p>我是子元素二:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长</p>
<p>我是子元素三:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长</p>
</div>
</body>
此处, div 元素的大小被限制为 200*200,其子元素的内容较多,超过了父元素的大小,需要渲染引擎将文字内容的一部分显示在div区域。 出现裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
总之,元素有了层叠上下文的属性或者需要被剪裁,满足这任意一点,就会被提升成为单独一层。
第五步:图层绘制
布局树生成图层树后,渲染引擎会依次对图层树中的图层进行绘制。接下来看看渲染引擎如何实现图层的绘制?
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
一般我们会把绘制操作分解为三步:
-
绘制蓝色背景;
-
在中间绘制一个红色的圆;
-
再在圆上绘制绿色三角形。
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的 绘制指令 ,然后再把这些指令按照顺序组成一个 待绘制列表, 如下图所示:
从图中可以看出,绘制指令其实非常简单,就是一个简单的绘制操作,比如绘制一个黑色的矩形。通常绘制一个元素需要多条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。 所以在图层绘制阶段,输出的内容就是一个绘制指令集合-待绘制列表。
第六步:栅格化(光栅化)
在第五步的图层绘制中,并没有真正对图层进行绘制,只是输出了一个记录了绘制顺序和绘制指令的列表,实际上绘制操作是由渲染引擎的合成线程来完成的。
当图层的绘制列表准备好之后,主线程会把该绘制列表提交给合成线程,那么接下来合成线程是怎么工作的呢?
在有些情况下,有的图层很大,内容很长,需要滚动很久才能滚动到底部,真正展示在视口内的内容只有一小部分,如果将整个图层完全绘制,就会产生很大的开销。为了降低绘制成本、尽可能快地呈现内容给用户,合成线程进行了一些优化。
6.1 划分图块
首先,合成线程会将图层划分为图块( tile ),图片的大小通常是 256256 或者 512512。 合成线程会按照视口附近的图块优先生成位图的原则将图块生成位图,而实际生成位图的操作是由栅格化来执行的。所谓栅格化,就是将图块转换为位图。
6.2 生成位图
图块是栅格化执行的最小单位。渲染进程维护了一个栅格化线程池,所有图块栅格化都是在此线程池中完成的。 流程如下图所示:
现代浏览器支持 GPU,通常会使用 GPU 加速栅格化的过程,使用 GPU 生成位图的过程叫快速栅格化,生成的位图保存在 GPU 内存中。
前文说到, GPU 操作有一个独立的进程——GPU 进程,如果栅格化操作使用了GPU,那么最终位图生成的操作理应是在 GPU 中完成的,这是一个跨进程、多进程协作的过程 :
渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,保存在 GPU 内存中。
第七步:合成和显示
所有图块完成栅格化后,合成线程会生成一个绘制图块的命令——"DrawQuad" 并提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将页面内容绘制到内存中,最后再将内存数据显示在屏幕上。
总结
结合上图,一个完整的渲染流程大致可总结为如下:
-
渲染进程将 HTML 内容转换为能够读懂的 DOM 树 结构。
-
渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets ,计算出 DOM 节点的样式。
-
创建 布局树 ,并计算元素的布局信息。
-
对布局树进行分层,并生成 分层树 。
-
为每个图层生成 绘制列表 ,并将其提交到合成线程。
-
合成线程将图层分成 图块 ,并在 光栅化 (栅格化) 线程池中将图块转换成位图。
-
合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
-
浏览器进程根据 DrawQuad 消息 生成页面 ,并 显示 到显示器上。
后话
梳理下来可以看出,Chrome 的渲染流水线还是非常复杂且难以理解的,深入了解其渲染原理可以帮助我们在工作中排查问题、调优等。
最后,笔者才疏学浅,如有错误之处请各位不吝赐教。