之前有研究过js的性能优化问题,但是感觉理解的不是很透彻,还是要从其根本看起,所以就研究一下浏览器相关比较深层次的东西。
为什么要了解浏览器的渲染机制
作为一个前端开发,最常见的运行环境应该就是浏览器,为了更好的通过浏览器把好的产品呈现给用户,也为了更好的发展前端之路,还是有必要了解一下我们在浏览器地址栏输入网址到看到页面这个期间,浏览器是如何运作的,进而了解如何更好的优化和实践。
介绍浏览器
主流浏览器
介绍浏览器内核之前,我们先过一下内核的概念:
浏览器内核又可以分为两部分:渲染引擎和js引擎。
渲染引擎负责取得网页的内容(html、xml、图像等)、整理讯息(加入css等)、以及计算网页的显示方式,然后输出至显示器或者打印机。浏览器内核不容对于网页的语法解释会有不同,所以渲染的效果也不同。
js引擎负责杰西javascript语言,执行js语言来实现网页的动态效果。
最开始玄滩引擎和js引擎并没有区分的很明确,后来js引擎越来越独立,内核就倾向于只指渲染引擎。
内核种类很多,但常见的四种:Trident、Gecko、Blink、Webkit。
下面介绍主流的浏览器(按照诞生顺序介绍)
1、IE(Internet Explorer)
IE的呈现引擎就是Trident,这也是大家俗称的IE内核,国内的大多数浏览器都有使用IE内核,或者是IE和Chrome双内核这样的形式来提高性能。除了一些传统企业和传统行业还在兼容ie6+外,一般都只兼容ie8+了。
2、Opera浏览器
Opera浏览器的内核最初是Presto,前几年宣布使用Google的开源项目Webkit作为自己的内核,没过多久,又跟随Google使用Blink内核。
3、safari
第二次浏览器大战基本是从苹果公司2003年1月发布其自有浏览器Safari开始的,苹果利用自己独天得厚的手机市场份额,使Safari浏览器的用户数量不断上升。从Safari推出之时起,它的渲染引擎就是Webkit,提到webkit,首先想到的便是chrome,但是webkit的鼻祖确是Safari。
4、Firefox浏览器:
在之前Mosai内科的基础上,开发了Gecko引擎,从firefox温室开始,第二次浏览器大战彻底打响了。
5、Chrome浏览器:
最早内核是webkit,后来使用了Blink引擎, 其实是webkit的一个分支。
1、IE浏览器内核:Trident内核,也是俗称的IE内核;
2、Chrome浏览器内核:统称为Chromium内核或Chrome内核,以前是Webkit内核,现在是Blink内核;
3、Firefox浏览器内核:Gecko内核,俗称Firefox内核;
4、Safari浏览器内核:Webkit内核;
5、Opera浏览器内核:最初是自己的Presto内核,后来加入谷歌大军,从Webkit又到了Blink内核;
6、360浏览器、猎豹浏览器内核:IE+Chrome双内核;
7、搜狗、遨游、QQ浏览器内核:Trident(兼容模式)+Webkit(高速模式);
8、百度浏览器、世界之窗内核:IE内核;
9、2345浏览器内核:好像以前是IE内核,现在也是IE+Chrome双内核了;
10、UC浏览器内核:这个众口不一,UC说是他们自己研发的U3内核,但好像还是基于Webkit和Trident,还有说是基于火狐内核。。
浏览器基础结构
浏览器的基础结构包括以下7个部分:
- 用户界面:用户所看到的功能组件,入地址栏,返回,前进按钮等;
- 浏览器引擎:责付和控制下一级的渲染引擎;
- 渲染引擎:负责杰西用户请求内容;
- 网络:负责处理网络相关的事务,如http请求等;
- UI后端:负责绘制提示款等浏览器组件,其底层使用的事操作系统的用户接口;
- JavaScript解释器:负责加息和执行JavaScript代码;
- 数据存储:负责持久存储cookie和缓存等应用数据。
网络
当用户访问页面时,浏览器需要获取用户请求内容,这个过程主要涉及浏览器的网络模块:
- 用户在地址栏输入域名。比如baidu.com,DNS(Domain Name System,域名解析系统)服务器根据输入的域名查找对应IP,然后向该IP地址发起请求;
- 浏览器活的并解析服务器的返回内容(HTTP response);
- 浏览器加载HTML文件及文件内包含的外部引用文件及图片,多媒体等资源。
DNS预解析
浏览器DNS解析大多时候比较快,且会缓存常用域名的解析之,但是如果网站设计多域名,在对每一个域名访问时都需要县解析出IP地址,而我们希望在跳转或者请求域名资源时尽量快,则可以开启域名与解析。浏览器会在空闲时提前解析生命需要预解析的。
多进程
JavaScript是单进程的,但是浏览器网络部分通常是由几个平行进程同事开启,但是也有限制,一般为2-6个。
浏览器的渲染
基本流程:构建dom树—->构建render树—->绘制render树
具体如下:
- 构建DOM树:从上到下解析HTML文档生成Dom节点树(dom tree)也叫内容树(content tree);
- 构建CSSOM(css Object Model)树:加载解析样式生成CSSOM树;
- 执行JavaScript:加载并执行JavaScript代码(包括内联代码或外联JavaScript文件);
- 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树;
- 布局:根据渲染树将节点树的每一个节点布局在屏幕上的正确位置;
绘制:遍历渲染树绘制所有节点,为每一个节点使用对应的样式,这一过程是通过IO后端模块完成;
为了更友好的体验,浏览器会尽可能快地展现内容,而不会等到文档所有内容都拿到之后才开始解析和构建渲染树。
Webkit渲染引擎流程图:
Gecko渲染引擎流程图:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./css//demo.css"></link>
<title>我是标题我是标题</title>
</head>
<body>
<h1 class="title">关键渲染路径</h1>
<p>关键渲染路径介绍</p>
<footer>amber wu</footer>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="./js//demo.js"></script>
</body>
</html>
html, body {
width: 100%;
height: 100%;
background-color: #fcfcfc;
}
.title {
font-size: 20px;
color: aqua
}
.footer {
font-size: 12px;
color: #aaa;
}
以上html内容+css内容构DOM树和CSSOM树:
执行JavaScript
脚本加载,解析和执行会阻塞文档解析,而在特殊情况下样式的加载和解析也会阻塞脚本,所以现在推荐的实践是
构建渲染树
渲染树,代表一个文档的视觉展示,浏览器通过它将文档内容绘制在浏览器窗口,展示给用户,它由按顺序展示在屏幕上的一系列矩形对象组成,这些矩形对象都带有字体,颜色和尺寸,位置等视觉样式属性。对于这些矩对象,FireFox称之为框架(frame),Webkit浏览器称之为渲染对象(render object, renderer),后文统称为渲染对象。
渲染树及其对应的DOM树如图:
布局(Layout)或回流(reflow,relayout)
创建渲染树后,下一步就是布局(Layout)或者叫做回流(reflow,relayout),这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置。但有些时候,我们会在文档布局完成后对DOM进行修改,这时候可能需要重新进行布局,也可称其为回流。本质上还是一个布局的过程。每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。
中间还有几个小定义:
流(flow)
HTML采用的是基于流的方式定位布局,其按照从左到右,从上到下的顺序进行排列。
全局布局与局部布局
对render tree的布局可以分为全局和局部的。全局即对整个渲染树进行重新布局,比如,我们改变窗口尺寸或者方向,或者是修改了根元素的尺寸或者字体大小等;而布局布局可以使对渲染树的某部分或某个渲染对象进行重新布局。
布局过程
布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素,然后下一句渲染对象,如对应着元素,如此层层递归,一次计算每一个渲染对象的几何信息(位置和尺寸)——即相对于窗口的坐标和尺寸,如根渲染对象,其坐标为(0,0),尺寸即是视口尺寸(浏览器窗口的可视区域)。
每一个渲染徐娘的布局流程:
强制回流
在渲染树布局完成后,再次操作文档,改变文档的内容或结构,或者元素定位时,会出发回流,就需要重新布局。
- DOM操作,如增加、删除、修改或移动;
- 变更内容;
- 激活伪类;
- 访问或改变某些css属性(宝具哦修改样式表或元素名或使用JavaScript操作等方式);
- 浏览器窗口变化(滚动活尺寸变化)
之前看过一个例子,
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./css//demo.css"></link>
<title>我是标题我是标题</title>
</head>
<body>
<div class="demo">demo</div>
</body>
</html>
css:
.slide-left {
width: 100px;
height: 100px;
background: pink;
}
.slide-left2 {
-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;
}
js:
var $slide = $('.slide-left');
$slide.css({
"margin-left": "500px"
}).addClass('slide-left2');
$slide.css({
"margin-left": "50px"
});
这段代码没有产生我们想要的效果。而下面这段代码:
var $slide = $('.slide-left');
$slide.css({
"margin-left": "500px"
});
console.log($slide.css('padding');
$slide.addClass('slide-left2');
$slide.css({
"margin-left": "70px"
});
会产生我们想要的效果。
插曲:
一开始我以为是console.log会自行引起回流,结果发现我大错特错了。
因为将console.log($slide.css('padding');
改成$slide.css('padding');
会产生同样的效果。
后来我又假设,是获取padding值的时候,引起了offsetHeight或者offsetWidth的变化(科科,肯定无稽之谈)。
再后来,在大神指点下,看了一下源码。发现jQuery
的CSS()
方法底层运作其实就是getComputedStyle以及getPropertyValue方法。(这里可以参考张鑫旭大神的文章)
于是乎,我把代码改成了var a = getComputedStyle($slide[0]).getPropertyValue("padding");
果然会产生效果。
绘制(painting)
最后是绘制(paint)阶段或重绘(repaint)阶段,浏览器UI组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对DOM进行修改,需要重新绘制渲染对象,也就是重绘。
绘制和重绘的关系可以参考布局和回流的关系。
全局绘制与局部绘制
与布局相似,绘制也分为全局和局部绘制,即对整个渲染树或某些渲染对象进行绘制。
触发重绘
通常,当改变元素的视觉样式会触发全局活局部重绘。
var s = document.body.style;
s.backgroundColor = "#ccc"; // 重绘
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));
由此可见,回流必将引起重绘,而重绘不一定会引起回流。
回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系。
比如:
display:none
的标签不会被加入Render Tree,也不会触发reflow,
visibility:hidden
的标签会被加入到Render Tree,不会触发reflow,只会触发repaint。
在body前面插入一个元素,讲会导致整个render tree回流,这样代价相当高,
但是如果只是在body后面插入一个元素,则不会影响前面元素的回流。‘
由此我们可以得出一些页面渲染优化的注意点:
- HTML文档结构层次尽量少,最好不要深于6层;
- 脚本尽量后放,放在;
- 少量首屏样式内联放在标签内;
- 样式结构层次尽量简单;
- 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度出发回流;
- 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式活动画;
- 动画尽量使用在绝对定位或固定定位的元素上;
- 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
- 尽量缓存DOM查找,查找器尽量简洁;
- 设计多域名的网站,可以开启域名与解析。
- 如果需要频繁的修改DOM样式,尽量通过预先定义好的css的clsss,然后修改DOM的className。
- 不要把DOM节点的属性值放在一个循环里当成循环里的变量
- 为需要添加动画的HTML元素,添加上position:absolute/fixed属性值,这样修改该元素的css是不会引起reflow。
- 不要使用table布局,因为可能一个很小的改动就会引起整个table的重新布局。
有点累了 歇歇。