浏览器渲染流程
对于 web 前端开发者,浏览器再熟悉不过了。我们可能都知道浏览器含有一个渲染引擎,浏览器会根据
HTML
文件来进行解析与渲染,最终再将页面呈现在用户面前。但是其具体的渲染原理和流程估计也有很多同学不清楚。然而前端开发者很有必要了解浏览器的工作工作机制。
1. 先聊聊浏览器
浏览器可以被认为是使用最广泛的软件,在我们了解渲染机制之前,我们先来了解了解我们再熟悉不过的浏览器,浏览器的主要功能是将用户所请求的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式
1.1 主流浏览器
从 StatCounter 的最新数据我们可以看到,现在市面有六种主流桌面浏览器:Chrome、Firefox、IE、Safari、Edge、Opera。如果是全平台 UC Browser 也是占了很大的市场。(看着 IE 的占有率越来越低 Chrome 占有率越来越高,也甚是欣慰 ,当然了国内情可能会差一点)
1.2 浏览器的主要构成
浏览器的主要组件包括:
- 用户界面 (User Interface) - 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分。
- 浏览器引擎 (Browser engine) - 用来查询及操作渲染引擎的接口。
- 渲染引擎 (Rendering engine) - 负责解析用户请求的内容(如 HTML 或 XML,渲染引擎会解析 HTML 或 XML,以及相关 CSS,然后返回解析后的内容)
- 网络(Networking)- 用来完成网络调用,例如 http 请求,它具有平台无关的接口,可以在不同平台上工作。
- UI 后端(UI backend) - 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。
- JS 解释器(JavaScript interpreter) - 用来解释执行 JS 代码。
- 数据存储 (Data storage) - 属于持久层,浏览器需要在硬盘中保存类似 cookie 的各种数据,HTML5 定义了 web database 技术,这是一种轻量级完整的客户端存储技术
1.3 输入网址到页面呈现
输入网址到页面呈现大致有下面五步:
- DNS 查询
- TCP 连接
- HTTP 请求即响应
- 服务器响应
- 客户端渲染
本篇文章的重点就在第五步即客户端渲染即浏览器渲染
浏览器渲染
渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。默认情况下,渲染引擎可以显示 html、xml 文档及图片,它也可以借助插件(一种浏览器扩展)显示其他类型数据,例如使用 PDF 阅读器插件,可以显示 PDF 格式
浏览器内核
各大主要浏览器使用内核也是有差别的,大致可以分为以下几类:
- Trident 内核: IE
- Gecko 内核:FireFox
- Webkit 内核:Chrome,Safari
关键渲染路径
当浏览器接收到服务器接返回的一个 HTML 页面,到屏幕上渲染出来要经过很多个步骤。浏览器完成这一系列的运行(即渲染出来),我们称之为“关键渲染路径”(Critical Rendering Path) 下面是渲染引擎在取得内容之后的基本流程:
// / 执行JavaScript \
// / \
// 解析html以构建dom树 -> 解析css构建cssom树 -> 构建render树 -> 布局render树 -> 绘制render树
接下来我们认识几个概念:
DOMTree
:浏览器将 HTML 解析成树形的数据结构。CSSRuleTree
:浏览器将 CSS 解析成树形的数据结构。RenderTree
: DOM 和 CSSOM 合并后生成 Render Tree。layout
: 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置。painting
: 按照算出来的规则,通过显卡,把内容画到屏幕上。reflow
(回流):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫 reflow。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。repaint
(重绘):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
渲染流程图
Webkit 渲染引擎流程如下图:
Gecko 渲染引擎流程如下图:
从上面两个图可以看出,尽管 webkit 和 Gecko 使用的术语稍有不同,他们的主要流程基本相同:
- 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
- 将 CSS 解析成 CSS Rule Tree 。
- 根据 DOM 树和 CSS Rule Tree 来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。
- 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步操作称之为 layout,顾名思义就是计算出每个节点在屏幕中的位置。
- 再下一步就是绘制,即遍历 render 树,并使用 UI 后端层绘制每个节点。
二者不同之处:
- Gecko 称可见的格式化元素组成的树为 frame 树,webkit 则使用 render 树这个名词来命名由渲染对象组成的树。
- Webkit 中元素的定位布局称为布局,而 Gecko 中称为回流。
- Webkit 称利用 dom 节点及样式信息去构建 render 树的过程为 attachment,Gecko 在 html 和 dom 树之间附加了一层,这层称为内容池,相当制造 dom 元素的工厂
下面将讨论流程中的各个阶段
构建 DOM 树
文档对象模型 (DOM) 是 HTML 和 XML 文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将 web 页面和脚本或程序语言连接起来。
浏览器从网络或硬盘中获得 HTML
字节数据后会经过一个流程将字节解析为 DOM
树
假设浏览器获取返回的如下 HTML 文档:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./theme.css">
</link>
<script src="./config.js"></script>
<title> 关键渲染路径 </title>
</head>
<body>
<h1 class="title"> 关键渲染路径 </h1>
<p> 关键渲染路径介绍 </p>
<footer> @copyright2017 </footer>
</body>
</html>
首先浏览器从上到下依次解析文档构建 DOM 树,如下:
构建 CSSOM 树
CSSOM
树全称为CascadingStyleSheetsObjectModel
层叠样式表对象模型,它与DOM
树的含义相差不大,只不过它是CSS
的对象集合。
浏览器获得外部 CSS
文件的数据后,就会像构建 DOM
树一样开始构建 CSSOM
树,这个过程没有什么特别的差别。
css 样式内容如下:
html, body {
width: 100%;
height: 100%;
background-color: #fcfcfc;
}
.title {
font-size: 20px;
}
.footer {
font-size: 12px;
color: #aaa;
}
构建 CSSOM 树如图:
为了更直观的看见这个关键渲染路径的构建,我们可以使用 Chrome
开发者工具中的Performance (即 Timeline)功能来查看,下图是打开的是百度首页:
打开 Event Log 查看具体渲染过程
创建 Render 树
在构建了 DOM
树和 CSSOM
树之后,浏览器只是拥有了两个互相独立的对象集合, DOM
树描述了文档的结构与内容, CSSOM
树则描述了对文档应用的样式规则,想要渲染出页面,就需要将 DOM 树与 CSSOM 树结合在一起,构成渲染树
渲染树及其对应 DOM 树如图:
- 图中渲染树 viewport 即视口,是文档的初始包含块,scroll 代表滚动区域
- 浏览器会先从
DOM
树的根节点开始遍历每个可见节点(不可见的节点自然就没必要渲染到页面了,不可见的节点还包括被CSS
设置了display:none
属性的节点,值得注意的是visibility:hidden
属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,所以它会被渲染成一个空框)。 - 对每个可见节点,找到其适配的
CSS
样式规则并应用。 - 渲染树构建完成,每个节点都是可见节点并且都含有其内容和对应规则的样式。
渲染树构建完毕后,浏览器得到了每个可见节点的内容与其样式,下一步工作则需要计算每个节点在窗口内的确切位置与大小,也就是布局阶段或者叫回流(reflow,relayout)
布局(Layout)或回流(reflow,relayout)
这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置,而有些时候我们会在文档布局完成后对 DOM 进行修改,这时候可能需要重新进行布局,也可称其为回流,本质上还是一个布局的过程,每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。
流(flow)
HTML 采用的是基于流的方式定位布局,其按照从左到右,从上到下的顺序进行排列,详见 CSS 定位机制。
全局布局与局部布局
对渲染树的布局可以分为全局和局部的,全局即对整个渲染树进行重新布局,如当我们改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等;而局部布局可以是对渲染树的某部分或某一个渲染对象进行重新布局。
脏位系统(dirty bit system)
大多数 web 应用对 DOM 的操作都是比较频繁,这意味着经常需要对 DOM 进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的,为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。
表示需要布局的脏位值有两种:
- “dirty”–自身改变,需要回流
- “children are dirty”–子节点改变,需要回流
布局过程
布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,即对应着 HTML 文档根元素 <html>
,然后下一级渲染对象,如对应着 <body>
元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)
CSS
采用了一种叫做盒子模型的思维模型来表示每个节点与其他元素之间的距离,它包括:边距,边框,填充,和实际内容,页面中的每个标签其实都是一个个盒子。布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对的测量值也都会被转换为屏幕内的绝对像素值。
每一个渲染对象的布局流程基本如:
- 1.计算此渲染对象的宽度(width);
- 2.遍历此渲染对象的所有子级,依次:
- 2.1 设置子级渲染对象的坐标
- 2.2 判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)
- 3.设置此渲染对象的高度:根据子渲染对象的累积高,margin 和 padding 的高度设置其高度;
- 4.设置此渲染对象脏位值为 false。
强制回流
在渲染树布局完成后,再次操作文档,改变文档的内容或结构,或者元素定位时,会触发回流,即需要重新布局如:
- DOM 操作,如增加,删除,修改或移动;
- 变更内容;
- 激活伪类;
- 访问或改变某些 CSS 属性(包括修改样式表或元素类名或使用 JavaScript 操作等方式);
- 浏览器窗口变化(滚动或尺寸变化)
绘制(painting)
最后是绘制(paint)阶段或重绘(repaint)阶段,浏览器 UI 组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对 DOM 进行修改,需要重新绘制渲染对象,也就是重绘,绘制和重绘的关系可以参考布局和回流的关系。
全局与局部绘制
与布局相似,绘制也分为全局和局部绘制,即对整个渲染树或某些渲染对象进行绘制。
触发重绘
我们已经知道很多操作可能会触发回流,那么什么时候可能触发重绘呢,通常,当改变元素的视觉样式,如 background-color
, visibility
, margin
, padding
或 color
时会触发全局或局部重绘。
现在我们来回顾下浏览器关键渲染路径的整个过程:
- 处理
HTML
标记数据并生成DOM
树。 - 处理
CSS
标记数据并生成CSSOM
树。 - 将
DOM
树与CSSOM
树合并在一起生成渲染树。 - 遍历渲染树开始布局,计算每个节点的位置信息。
- 将每个节点绘制到屏幕。
本篇文章我们讲了浏览器的渲染机制,理解这有助于我们更好的做性能优化.