首先,你应该了解的就是,浏览器是如何渲染一个页面的。
先看一个大致的流程图
它的总体流程是这样的:
1)浏览器解析这三个东西:
- 解析HTML/XHTML/SVG,生成DOM树(事实上,Webkit有三个C++的类对应这三类文档以用于解析)。
- 解析css文件产生CSS Rule树(css规则树)。
- 解析javascript,通过DOM API和CSSOM API来操作DOM树和CSS Rule树。
2)解析完成后,浏览器会根据DOM树和CSS Rule树来构造渲染树(Rendering Tree)。
- 渲染树并不完全等同于DOM树,因为一些display:none的东西就没必要放在渲染树中了。
- CSS Rule树主要是为了完成匹配并把CSS Rule附加到渲染树上的每个DOM结点。
- 然后,计算每个DOM节点的位置,这又叫layout和reflow过程。
3)最后通过调用操作系统Native GUI的API绘制(painting)。
抛去其中的细节,再简单一点的说法就是:DOM树解析->css解析->渲染(也就是构建渲染树以及最终呈现到浏览器上的过程)
这里 主要针对第三步的渲染过程进行一下讲解:
- 计算css样式,这一步对应着总体流程中的这句话,CSS Rule树主要是为了完成匹配并把CSS Rule附加到渲染树上的每个DOM结点,也就是说,最终的渲染树映射了 DOM 的结构。在渲染树中,每一个文本字符串都被当做一个独立的 renderer。每个渲染对象都包含了与之对应的计算过样式的DOM 对象(或者一个文本块)。换句话说,渲染树描述了 DOM 的直观的表现形式。
- 构建Render Tree。
- Layout – 定位坐标和大小,是否换行,各种position, overflow, z-index属性 ……
- 正式开画
需要注意的是:Javascript如果动态修改了DOM属性或是CSS属会导致重新Layout(Reflow),当然有些属性改变不会。
这里,这里重要要说两个概念,一个是Reflow,另一个是Repaint
Repaint(重绘)
当在页面上修改了一些不需要改变定位的样式的时候(比如background-color
,border-color
,visibility
),浏览器只会将新的样式重新绘制给元素(这就叫一次“重绘”或者“重新定 义样式”)。这时只需要屏幕的一部分要重画。
Reflow(重排)
当页面上的改变影响了文档内容、结构或者元素定位时,就会发生重排(或称“重新布局”)。重排通常由以下改变触发:
- DOM 操作(如元素增、删、改或者改变元素顺序)
- 内容的改变,包括 Form 表单中文字的变化
- 计算或改变 CSS 属性
- 增加或删除一个样式表
- 浏览器窗口的操作(改变大小、滚动窗口)
- 激活伪类(如:hover状态)
这时,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式 布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在 reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。
可以看出,这两个动作对于浏览器的性能都有较大的影响,当然reflow的成本比repaint的成本高好多。那么,浏览器又是如何避免成本增加,从而优化渲染的呢?
浏览器如何优化渲染?
1、浏览器尽最大努力限制重排
的过程仅覆盖已更改的元素的区域。举个例子,一个 position 为 absolue 或 fixed 的元素的大小变化只影响它自身和子孙元素,而对一个 position 为 static 的元素做同样的操作就会引起所有它后面元素的重排。
2、当运行一段Jjavascript 代码的时候,浏览器会将一些修改缓存起来,然后当代码执行的时候,一次性的将这些修改执行。举例来说,这段代码会触发一次重绘和一次重排:
var bstyle = document.body.style; // cache bstyle.padding = "20px"; // reflow, repaint bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint bstyle.color = "blue"; // repaint bstyle.backgroundColor = "#fad"; // repaint bstyle.fontSize = "2em"; // reflow, repaint // new DOM element - reflow, repaint document.body.appendChild(document.createTextNode('dude!'));
浏览器不会像上面那样,你每改一次样式,它就reflow或repaint一次。一般来说,浏览器会把这样的(都是设置style属性,而不涉及其他类似读取属性的操作)操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。
但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些DOM值:(比如我们在上面的例子中若加一个读取属性的操作则会引起又一次的重排)
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- IE中的 getComputedStyle(), 或 currentStyle
因为,如果我们的程序需要这些值,那么浏览器需要返回最新的值,而这样一样会flush出去一些样式的改变,从而造成频繁的reflow/repaint。
当然,我们可以通过改变书写习惯而做一些认为的性能优化:
实际优化建议
- 创建合法的 HTML 和 CSS ,别忘了制定文件编码,Style 应该写在 head 标签中,script 标签应该加载 body 标签结束的位置
- 试着简化和优化 CSS 选择器(这个优化点被大多数使用 CSS 预处理器的开发者忽略了)。将嵌套层数控制在最小。
- 在你的脚本中,尽可能的减少 DOM 的操作。把所有东西都缓存起来,包括属性和对象(如果它可被重复使用)。进行复杂的操作的时候,最好操作一个“离线”的元素(“离线”元素的意思是与 DOM 对象分开、仅存在内存中的元素),然后将这个元素插入到 DOM 中。
例如:
1、使用documentFragment 对象在内存里操作DOM,类似以下的代码示例:
// Create the fragment var fragment = document.createDocumentFragment(); //add DOM to fragment for(var i = 0; i < 10; i++) { var spanNode = document.createElement("span"); spanNode.innerHTML = "number:" + i; fragment.appendChild(spanNode); } //add this DOM to body document.body.appendChild(spanNode);
2、先把DOM给display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
3、clone一个DOM结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下
- 不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,然后修改DOM的className
- 尽可能的只对 position 为 absolute 或 fix 的元素做动画
- 当滚动时禁用一些复杂的
:hover
动画是一个很好的主意(例如,给 body 标签加一个 no-hover 的 class - 千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局