本文大量引用其他网站的内容,原文链接:(http://blog.codingplayboy.com/2017/03/29/webpage_render/)
今天对web页面的渲染机制进行总结一下
浏览器基础结构
浏览器主要分为7部分:
- 用户界面(User Interface):用户所看到及与之交互的功能组件,如地址栏,返回,前进按钮等;
- 浏览器引擎(Browser engine):负责控制和管理下一级的渲染引擎;
- 渲染引擎(Rendering engine):负责解析用户请求的内容(如HTML或XML,渲染引擎会解析HTML或XML,以及相关CSS,然后返回解析后的内容);
- 网络(Networking):负责处理网络相关的事务,如HTTP请求等;
- UI后端(UI backend):负责绘制提示框等浏览器组件,其底层使用的是操作系统的用户接口;
- JavaScript解释器(JavaScript interpreter):负责解析和执行JavaScript代码;
- 数据存储(Data storage):负责持久存储诸如cookie和缓存等应用数据。
主流浏览器内核
主流浏览器内核主要有3种:
- Trident内核: IE
- Webkit内核:Chrome,Safari
- Gecko内核:FireFox
DNS预解析(DNS prefetch)
DNS一般会被浏览器缓存,但是网站里有多种域名时,我们想让浏览器解析速度比较快,就需要用到域名预解析(rel=’dns-prefetch’),如
<link rel='dns-prefetch' href='http://www.baidu.com' />
多进程加载资源
通常浏览器加载资源时多进程的,一般为同时加载2-6ge资源。
以上为热身运动,下边讲解浏览器的渲染机制
渲染引擎及关键渲染路径(Critical Rendering Path)
渲染引擎做的事情是将请求到的内容展现,默认支持HTML、XML、图片等
关键渲染路径包含:
- 构建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后端模块完成;
页面渲染流程图:
1. Webkit渲染引擎流程如下图:
2. Gecko渲染引擎流程如下图:
chrome和火狐的渲染区别为:
1. Webkit浏览器中的渲染树(render tree),在Gecko浏览器中对应的则是框架树(frame tree),渲染对象(render object)对应的是框架(frame);
2. Webkit中的布局(Layout)过程,在Gecko中称为回流(Reflow),本质是一样的,后文会解释回流的另一层含义–重新布局;
3. Gecko中HTML和DOM树中间多了一层内容池(Content sink),可以理解成生成DOM元素的工厂。
渲染步骤是一步一步同步进行的
解析文档顺序
浏览器按从上到下的顺序扫描解析文档;
解析样式和脚本
- 脚本或许是由于通常会在JavaScript脚本中改变文档DOM结构,于是浏览器以同步方式解析,加载和执行脚本,浏览器在解析文档时,当解析到script标签时,会解析其中的脚本(对于外链的JavaScript文件,需要先加载该文件内容,再进行解析),然后立即执行,这整个过程都会阻塞文档解析,直到脚本执行完才会继续解析文档。就是说由于脚本是同步加载和执行的,它会阻塞文档解析,这也解释了为什么现在通常建议将script标签放在最后边。现在HTML5提供defer(延迟)和async(异步)两个属性支持延迟和异步加载JavaScript文件,将不操作dom的脚本变为异步加载,如:
补充defer和async的区别:
<script defer src="script.js">
- 改进针对上文说的脚本阻塞文档解析,主流浏览器如Chrome和FireFox等都有一些优化,比如在执行脚本时,开启另一个进程解析剩余的文档以找出并加载其他的待下载外部资源(不改变主进程的DOM树,仅优化加载外部资源)。
- 样式不同于脚本,浏览器对样式的处理并不会阻塞文档解析,大概是因为样式表并不会改变DOM结构。
- 样式表与脚本你可能想问样式是否会阻塞脚本文件的加载执行呢?正常情况是不会的,但是存在一个问题是通常我们会在脚本中请求样式信息,但是在文档解析时,如果样式尚未加载或解析,将会得到错误信息,对于这一问题,FireFox浏览器和Webkit浏览器处理策略不同:
- 当存在有样式文件未被加载和解析时,FireFox浏览器会阻塞所有脚本;
- 而Webkit浏览器只会阻塞操作了改文件内声明的样式属性的脚本。
构建DOM树
DOM,即文档对象模型(Document Object Model),DOM树,即文档内所有节点构成的一个树形结构。例如:
<!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,即CSS对象模型(CSS Object Model),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标签放在最后边。
构建渲染树(render tree)
DOM树和CSSOM树都构建完了,接着浏览器会构建渲染树:
渲染树,代表一个文档的视觉展示,浏览器通过它将文档内容绘制在浏览器窗口,展示给用户,它由按顺序展示在屏幕上的一系列矩形对象组成,这些矩形对象都带有字体,颜色和尺寸,位置等视觉样式属性。对于这些矩对象,FireFox称之为框架(frame),Webkit浏览器称之为渲染对象(render object, renderer),后文统称为渲染对象。
这里把渲染树节点称为矩形对象,是因为,每一个渲染对象都代表着其对应DOM节点的CSS盒子,该盒子包含了尺寸,位置等几何信息,同时它指向一个样式对象包含其他视觉样式信息。
渲染树与DOM树
每一个渲染对象都对应着DOM节点,但是非视觉(隐藏,不占位)DOM元素不会插入渲染树,如元素或声明display: none;的元素,渲染对象与DOM节点不是简单的一对一的关系,一个DOM可以对应一个渲染对象,但一个DOM元素也可能对应多个渲染对象,因为有很多元素不止包含一个CSS盒子,如当文本被折行时,会产生多个行盒,这些行会生成多个渲染对象;又如行内元素同时包含块元素和行内元素,则会创建一个匿名块级盒包含内部行内元素,此时一个DOM对应多个矩形对象(渲染对象)。
渲染树及其对应DOM树如图:
- 图中渲染树viewport即视口,是文档的初始包含块,scroll代表滚动区域,详见CSS之视觉格式化模型(Visual Formatting Model)
- 渲染树并不会包含显式或隐式地display:none;的标签元素。
布局(Layout)或回流(reflow,relayout)
布局为chrome的叫法,回流为火狐的叫法,表达的意思基本是一样的,这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置,而有些时候我们会在文档布局完成后对DOM进行修改,这时候可能需要重新进行布局,也可称其为回流,本质上还是一个布局的过程,每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。
对渲染树的布局可以分为全局和局部的,全局即对整个渲染树进行重新布局,如当我们改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等;而局部布局可以是对渲染树的某部分或某一个渲染对象进行重新布局。
脏位系统(dirty bit system)
大多数web应用对DOM的操作都是比较频繁,这意味着经常需要对DOM进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的,为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。
表示需要布局的脏位值有两种:
- “dirty”–自身改变,需要回流
- “children are dirty”–子节点改变,需要回流
布局过程
布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素,然后下一级渲染对象,如对应着元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。
几何信息-位置和尺寸,即相对于窗口的坐标和尺寸,如根渲染对象,其坐标为(0, 0),尺寸即是视口
尺寸(浏览器窗口的可视区域)。
每一个渲染对象的布局流程基本如:
1. 计算此渲染对象的宽度(width);
2. 遍历此渲染对象的所有子级,依次:
- 设置子级渲染对象的坐标
- 判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)
3. 设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;
4. 设置此渲染对象脏位值为false。
强制回流
触发回流的情况如下:
- DOM操作,如增加,删除,修改或移动;
- 变更内容;
- 激活伪类;
- 访问或改变某些CSS属性(包括修改样式表或元素类名或使用JavaScript操作等方式);
- 浏览器窗口变化(滚动或尺寸变化)
$('body').css('padding'); // reflow
$('body')[0].offsetHeight; // reflow
案例1:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>RunJS</title>
<script id="jquery_183" type="text/javascript" class="library" src="/js/sandbox/jquery/jquery-1.8.3.min.js"></script>
<style>
.slide-left {
-webkit-transition: margin-left 1s ease-out;
-moz-transition: margin-left 1s ease-out;
-o-transition: margin-left 1s ease-out;
transition: margin-left 1s ease-out;
}
</style>
</head>
<body>
<div class="block" style="width:100px;height:100px;background:#ff0000">
</div>
<button onclick="javascript:reflow();">回流测试-margin</button>
</body>
<script>
// 回流测试-margin
function reflow(){
// 没有触发回流,因此不能margin-left:100px不生效
var $slide = $('.block');
$slide.css({
"margin-left": "100px"
}).addClass('slide-left');
$slide.css({
"margin-left": "10px"
});
// 触发回流,因此margin-left会从100px到10px
/*var $slide = $('.block');
$slide.css({
"margin-left": "100px"
});
console.log($slide.css('padding'));
$slide.addClass('slide-left');
$slide.css({
"margin-left": "10px"
});*/
}
</script>
</html>
案例2:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>RunJS</title>
<script id="jquery_183" type="text/javascript" class="library" src="/js/sandbox/jquery/jquery-1.8.3.min.js"></script>
<style>
.madel{
width: 200px;
height: 200px;
position: absolute;
top: 50%;
left: 50%;
margin: -100px 0px 0px -100px;
background: yellow;
display: none;
-webkit-transition: all 1s ease-out;
-moz-transition: all 1s ease-out;
-o-transition: all 1s ease-out;
transition: all 1s ease-out;
}
.madel.show{
display: block;
transform: scale(.8,.8);
}
.madel.show-big{
transform: scale(1,1);
}
</style>
</head>
<body>
<div class="madel"></div>
<button onclick="javascript:displayReflow();">回流测试-display</button>
</body>
<script>
// 回流测试-display
function displayReflow(){
var $madel = $('.madel');
$madel.addClass('show');
$madel.css('padding');// 触发回流
$madel.addClass('show-big');
}
</script>
</html>
绘制(painting)
最后是绘制(paint)阶段或重绘(repaint)阶段,浏览器UI组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对DOM进行修改,需要重新绘制渲染对象,也就是重绘,绘制和重绘的关系可以参考布局和回流的关系。
全局与局部绘制与布局相似,绘制也分为全局和局部绘制,即对整个渲染树或某些渲染对象进行绘制。
触发重绘
我们已经知道很多操作可能会触发回流,那么什么时候可能触发重绘呢,通常,当改变元素的视觉样式,如background-color,visibility,margin,padding或字体颜色时会触发全局或局部重绘,如:
$('body').css('color', 'red'); // repaint
$('body').css('margin', '2px'); // reflow, repaint
以上是浏览器的渲染机制,下边写一下页面渲染优化
页面渲染优化
在改变文档根元素的字体颜色等视觉性信息时,会触发整个文档的重绘,而改变某元素的字体颜色则只触发特定元素的重绘;改变元素的位置信息会同时触发此元素(可能还包括其兄弟元素或子级元素)的布局和重绘。某些重大改变,如更改文档根元素的字体尺寸,则会触发整个文档的重新布局和重绘,据此及上文所述,推荐以下优化和实践:
- HTML文档结构层次尽量少,最好不深于六层;
- 脚本尽量后放,放在前即可;
- 少量首屏样式内联放在标签内;
- 样式结构层次尽量简单(降低优先级);
- 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;
- 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
- 动画尽量使用在绝对定位或固定定位的元素上(不影响其他元素);
- 页面滚动时,尽量停止动画;
- 尽量缓存DOM查找,查找器尽量简洁;
- 涉及多域名的网站,可以开启域名预解析
外部链接:
浅析前端页面渲染机制
渲染树render tree