###简介 了解从我们在浏览器地址栏输入网址到看到页面这期间浏览器是如何进行工作的,进而了解如何更好的优化实践是我们了解浏览器渲染原理的目的。 ###浏览器基础结构 目前主流五大浏览器:Chrome 、Firefox、IE、Safari、Opera,它们的基础结构都主要包括以下7部分:
- 用户界面,用户看到的功能组件,包括地址栏、前进/后退按钮、书签菜单等(除了浏览器主窗口显示的请求的页面外)
- 浏览器引擎: 在用户界面和渲染引擎之间传送指令
- 渲染引擎:负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络:用于网络调用,比如HTTP请求
- 用户界面后端:负责绘制提示框等浏览器组件,其底层使用的是操作系统的用户接口;
- JavaScript 解释器: 用于解析和执行 JavaScript 代码。
- 数据存储: 负责持久存储诸如cookie和缓存等应用数据。
###网络 用户在地址栏输入网址,发起HTTP请求,根据请求的URL进行域名解析,服务器接收到请求后返回HTML给浏览器。这个就是我们通常说的渲染页面,其实这只是浏览器渲染页面最开始的一部分。 ###渲染引擎 我们常说的浏览器内核也就是是渲染引擎(其实还包括js引擎,如WebKit,它由渲染引擎WebCore和javascript引擎JSCore组成)。渲染引擎所做的事是将请求内容展现给我们,默认支持HTML,XML和图片类型,对于其他诸如PDF等类型的内容则需要安装相应插件。不同的渲染引擎对网页编写语法的解释有所不同,进而导致同一个页面在不同内核的浏览器下显示出来的效果也会有所出入,这也是前端工程师需要让作品兼容各种浏览器的原因。简单说明下浏览器内核的作用,至于各浏览器内核的历程就不介绍了,有兴趣的可以深究。
通过网络请求到HTML文件后渲染引擎渲染流程如下
- 构建DOM树:从上到下解析HTML文档生成DOM树节点,也叫内容树
- 创建CSSOM树:加载解析样式生成CSSOM树
- 执行javascript:加载并执行javascript代码
- 构建渲染树:根据DOM树和CSSOM树,生成渲染树(render tree);按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性
- 布局(layout):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置
- 绘制:遍历渲染树并绘制每一个节点,绘制工作是通过用户界面后端完成的。
###流程图 ####解析文档阶段 解析文档是指将文档转化成为有意义的结构,也就是可让代码理解和使用的结构。解析得到的结果通常是代表了文档结构的节点树。
浏览器按从上到下的顺序扫描解析文档 #####HTML解析 HTML解析器最后输出的结构是由DOM元素和属性节点组成的DOM(Document Object Model)Tree,它是HTML文档的对象表示,也是外部内容(例如javascript)与HTML元素之间的接口 #####CSS解析 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。样式不会更改DOM,所以样式的加载是不需要阻塞文档加载的 #####javascript解析 主要是通过DOM API和CSSOM API来操作DOM Tree和CSSOM Tree,由于javascript脚本当中可能会有操作DOM的行为,所以脚本以同步方式解析。当遇到<script>标签时,会先解析脚本(外部文件的话会先加载再解析),然后执行脚本,等执行完才开始解析之后的文档,这个过程中,脚本是同步加载解析,会阻塞文档的解析,这也就是为什么脚本一般会放在底部,而不是头部中(defer和async属性例外),脚本在文档解析阶段可能会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 Webkit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
Webkit 和 Firefox进行了预解析这项优化, 在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。 ####渲染树 解析完成后,浏览器会根据DOM Tree和CSSOM Tree生成渲染树(Rendering Tree) ####绘制 调用操作系统Native GUI的API绘制。 ###构建DOM树 HTML的DOM Tree解析如下:
<html>
<html>
<head>
<title>Web page parsing</title>
</head>
<body>
<div>
<h1>Web page parsing</h1>
<p>This is an example Web page.</p>
</div>
</body>
</html>
上面这段HTML会解析成这样: ###CSS解析 ####Firefox 规则树、样式上下文树 为了简化样式计算,Firefox 采用了两种树:规则树和样式上下文树 HTML代码:
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
CSS代码:
div {margin:5px;color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
Firefox 会生成以下规则树:
样式上下文树:
####Webkit共享样式数据 Webkit 也有样式对象,但它们不是保存在类似样式上下文树这样的树结构中,而是直接把这个Style对象存在了相应的DOM结点上了 ###渲染树 DOM树和CSSOM树都构建完了,接着浏览器会构建渲染树,这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。让浏览器可以按照正确的顺序绘制内容
渲染树,代表一个文档的视觉展示,浏览器通过它将文档内容绘制在浏览器窗口,展示给用户,它由按顺序展示在屏幕上的一系列矩形对象组成,这些矩形对象都带有字体,颜色和尺寸,位置等视觉样式属性。对于这些矩对象,FireFox称之为框架(frame),Webkit浏览器称之为渲染对象(render object, renderer),后文统称为渲染对象。
渲染对象和DOM元素是相对应的,但并非一一对应,非可视化的 DOM 元素不会插入到渲染树。一个DOM可以对应一个渲染对象,但一个DOM元素也可能对应多个渲染对象,因为有很多元素不止包含一个CSS盒子,如当文本被折行时,会产生多个行盒,这些行会生成多个渲染对象;又如行内元素同时包含块元素和行内元素,则会创建一个匿名块级盒包含内部行内元素,此时一个DOM对应多个矩形对象(渲染对象)。 ###布局 创建渲染树后,下一步就是布局(Layout),或者叫回流(reflow,relayout),这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置(就像画画时我们如会先用铅笔在画布上把轮廓框架先画出来,然后再填色。),而有些时候我们会在文档布局完成后对DOM进行修改,这时候可能需要重新进行布局,也可称其为回流,本质上还是一个布局的过程,每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。 ####脏位系统(dirty bit system) 大多数web应用对DOM的操作都是比较频繁,这意味着经常需要对DOM进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的,为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。 ####布局过程 布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素<html>,然后下一级渲染对象,如对应着<body>元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。
- 计算此渲染对象的宽度(width);
- 遍历此渲染对象的所有子级,依次:
- 设置子级渲染对象的坐标
- 判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)
- 设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;
- 设置此渲染对象脏位值为false。 ####强制回流 在渲染树布局完成后,再次操作文档,改变文档的内容或结构,或者元素定位时,会触发回流,即需要重新布局,如请求某DOM的”offsetHeight”样式信息等诸多情况:
- DOM操作,如增加,删除,修改或移动;
- 变更内容;
- 激活伪类
- 访问或改变某些CSS属性(包括修改样式表或元素类名或使用JavaScript操作等方式);
- 浏览器窗口变化(滚动或尺寸变化)
###绘制 浏览器UI组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对DOM进行修改,需要重新绘制渲染对象,也就是重绘,绘制和重绘的关系可以参考布局和回流的关系。
当改变元素的视觉样式,如background-color,visibility,margin,padding或字体颜色时会触发全局或局部重绘。 ###页面渲染优化 在改变文档根元素的字体颜色等视觉性信息时,会触发整个文档的重绘,而改变某元素的字体颜色则只触发特定元素的重绘;改变元素的位置信息会同时触发此元素(可能还包括其兄弟元素或子级元素)的布局和重绘。某些重大改变,如更改文档根元素<html>的字体尺寸,则会触发整个文档的重新布局和重绘
- HTML文档结构层次尽量少,最好不深于六层;
- 脚本尽量后放,放在</body>前即可;
- 少量首屏样式内联放在<head>标签内;
- 样式结构层次尽量简单;
- 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;
- 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
- 动画尽量使用在绝对定位或固定定位的元素上;
- 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
- 尽量缓存DOM查找,查找器尽量简洁;
- 涉及多域名的网站,可以开启域名预解析