渲染引擎及关键渲染路径(Critical Rendering Path)
通过网络模块加载到HTML文件后渲染引擎渲染流程如下,这也通常被称作关键渲染路径(Critical Rendering Path)
- 构建DOM树(DOM tree):从上到下解析HTML文档生成DOM节点树(DOM tree),也叫内容树(content tree)
- 构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树
- 执行JavaScript:加载并执行JavaScript代码(包括内联代码或外联JavaScript文件)
- 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree)
- 布局(layout):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置
- 绘制(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式,这一过程是通过UI后端模块完成
为了更友好的用户体验,浏览器会尽可能快的展现内容,而不会等到文档所有内容到达才开始解析和构建/布局渲染树,而是每次处理一部分,并展现在屏幕上,这也是为什么我们经常可以看到页面加载的时候内容是从上到下一点一点展现的。
渲染引擎流程
Webkit渲染引擎流程如下图:
Gecko渲染引擎流程如下图:
如上图,Webkit浏览器和Gecko浏览器渲染流程大致相同,不同的是:
- Webkit浏览器中的渲染树(render tree),在Gecko浏览器中对应的则是框架树(frame tree),渲染对象(render object)对应的是框架(frame);
- Webkit中的布局(Layout)过程,在Gecko中称为回流(Reflow),本质是一样的,后文会解释回流的另一层含义–重新布局;
- Gecko中HTML和DOM树中间多了一层内容池(Content sink),可以理解成生成DOM元素的工厂。
单线程
渲染引擎是单线程工作的,意味着渲染流程是一步一步渐进完成的。
解析文档(PARSER HTML)
在详细介绍浏览器渲染文档之前,先应该理解浏览器如何解析文档:解析文档的顺序,对于CSS和JavaScript如何处理等。
解析顺序
浏览器按从上到下的顺序扫描解析文档;-
解析样式和脚本
- 脚本
由于通常会在JavaScript脚本中改变文档DOM结构,于是浏览器以同步方式解析,加载和执行脚本,浏览器在解析文档时,当解析到<script>标签时,会解析其中的脚本(对于外链的JavaScript文件,需要先加载该文件内容,再进行解析),然后立即执行,这整个过程都会阻塞文档解析,直到脚本执行完才会继续解析文档。就是说由于脚本是同步加载和执行的,它会阻塞文档解析,这也解释了为什么现在通常建议将
<script>
标签放在</body>
标签前面,而不是放在<head>
标签里。现在HTML5提供defer和async两个属性支持延迟和异步加载JavaScript文件,如:
- 脚本
<script defer src="script.js">
- 改进
针对上文说的脚本阻塞文档解析,主流浏览器如Chrome和FireFox等都有一些优化,比如在执行脚本时,开启另一个线程解析剩余的文档以找出并加载其他的待下载外部资源(不改变主线程的DOM树,仅优化加载外部资源)。
- 样式
不同于脚本,浏览器对样式的处理并不会阻塞文档解析,大概是因为样式表并不会改变DOM结构。
- 样式表与脚本
你可能想问样式是否会阻塞脚本文件的加载执行呢?正常情况是不会的,但是存在一个问题是通常我们会在脚本中请求样式信息,但是在文档解析时,如果样式尚未加载或解析,将会得到错误信息,对于这一问题,FireFox浏览器和Webkit浏览器处理策略不同:
- 当存在有样式文件未被加载和解析时,FireFox浏览器会阻塞所有脚本;
- 而Webkit浏览器只会阻塞操作了改文件内声明的样式属性的脚本。
构建DOM树
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树,与DOM树结构相似,只是另外为每一个节点关联了样式信息。
theme.css样式内容如下:
html, body {
width: 100%;
height: 100%;
background-color: #fcfcfc;
}
.title {
font-size: 20px;
}
.footer {
font-size: 12px;
color: #aaa;
}
构建CSSOM树如图:
执行JAVASCRIPT
上文已经阐述了文档解析时对脚本的处理,我们得知脚本加载,解析和执行会阻塞文档解析,而在特殊情况下样式的加载和解析也会阻塞脚本,所以现在推荐的实践是<script>
标签放在</body>
标签前面。
构建渲染树(RENDER TREE)
DOM树和CSSOM树都构建完了,接着浏览器会构建渲染树:
渲染树,代表一个文档的视觉展示,浏览器通过它将文档内容绘制在浏览器窗口,展示给用户,它由按顺序展示在屏幕上的一系列矩形对象组成,这些矩形对象都带有字体,颜色和尺寸,位置等视觉样式属性。对于这些矩对象,FireFox称之为框架(frame),Webkit浏览器称之为渲染对象(render object, renderer),后文统称为渲染对象。
这里把渲染树节点称为矩形对象,是因为,每一个渲染对象都代表着其对应DOM节点的CSS盒子,该盒子包含了尺寸,位置等几何信息,同时它指向一个样式对象包含其他视觉样式信息。
渲染树与DOM树
每一个渲染对象都对应着DOM节点,但是非视觉(隐藏,不占位)DOM元素不会插入渲染树,如<head>
元素或声明display: none;
的元素,渲染对象与DOM节点不是简单的一对一的关系,一个DOM可以对应一个渲染对象,但一个DOM元素也可能对应多个渲染对象,因为有很多元素不止包含一个CSS盒子,如当文本被折行时,会产生多个行盒,这些行会生成多个渲染对象;又如行内元素同时包含块元素和行内元素,则会创建一个匿名块级盒包含内部行内元素,此时一个DOM对应多个矩形对象(渲染对象)。
渲染树及其对应DOM树如图:
- 图中渲染树viewport即视口,是文档的初始包含块,scroll代表滚动区域
- 渲染树并不会包含显式或隐式地
display:none;
的标签元素。
布局过程
布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素<html>,然后下一级渲染对象,如对应着<body>元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。
几何信息-位置和尺寸,即相对于窗口的坐标和尺寸,如根渲染对象,其坐标为(0, 0),尺寸即是视口
尺寸(浏览器窗口的可视区域)。
每一个渲染对象的布局流程基本如:
- 计算此渲染对象的宽度(width);
- 遍历此渲染对象的所有子级,依次:
2.1 设置子级渲染对象的坐标
2.2 判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height) - 设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;
- 设置此渲染对象脏位值为false。
绘制(PAINTING)
最后是绘制(paint)阶段或重绘(repaint)阶段,浏览器UI组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对DOM进行修改,需要重新绘制渲染对象,也就是重绘,绘制和重绘的关系可以参考布局和回流的关系。
一个重要的概念reflow和repaint
- Repaint
屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
- Reflow
意味着元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。
Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。在一些高性能的电脑上也许还没什么,但是如果reflow发生在手机上,那么这个过程是非常痛苦和耗电的。
所以,下面这些动作有很大可能会是成本比较高的。
- 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint
- 当你移动DOM的位置,或是搞个动画的时候。
- 当你修改CSS样式的时候。
- 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。
- 当你修改网页的默认字体时。
注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。
减少reflow/repaint
- 不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,然后修改DOM的className。
- 不要把DOM结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
- 尽可能的修改层级比较低的DOM。当然,改变层级比较低的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。
- 为动画的HTML元件使用fixed或absoult的position,那么修改他们的CSS是不会reflow的。
- 千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。