先说清楚两个概念:
重绘Repaint 和重排Reflow
浏览器下载完毕所有的页面组件--包括html标记,js,css,图片后,会解析并生成两个内部的数据结构:
- DOM树
- 渲染树
DOM树都很熟悉了,而渲染树就是DOM树中每一个节点所对应的frame或者box所组成的(不包括隐藏的DOM元素)。DOM的变化影响了元素的几何属性的时候,比如大小宽高,从而影响了其他元素的几何属性和位置,这个时候浏览器就需要重新构造渲染树,这个过程就叫做重排,完成重排以后,浏览器再重新绘制受影响的部分到屏幕中,这个就叫做重绘。有的的变化不会影响几何属性的,比如改变背景色,就只会发生重绘,而不需要重排,因为元素的布局并没有发生改变,但这两种操作都是代价昂贵的,虽然浏览器本身会进行优化,但是在编写代码的时候仍然需要最少的去进行这样的操作。
因为详细说性能优化的足够可以写一本书了,所以本文只是在前文基础之上总结一些常见的方法:
合理的html结构和脚本的加载
由于浏览器往往使用单一进程来处理界面刷新和js脚本执行,意味着script标签会很霸道的让页面等待解析和执行完毕后才继续之后的下载和渲染,因此很典型的做法就是把脚本文件放在body的后部,把样式文件放在head标签中。如果把脚本放在页面顶部会导致明显的延迟,通常表现为白屏,用户看不到任何内容也无法进行任何的交互。
另外由于htttp请求会带来额外的性能开销,比如下载单个100kb的文件比下载4个25kb的文件要快,因此减少script和link标签的数量也是非常有必要的。关于这一点现在也有很多方便的打包压缩工具来实现了,就不赘述了。
值的一提的是因为web app的流行,现在的业务需求往往导致js代码量越来越多,所以精简代码和减少http请求往往只是优化的第一步,很多时候这是有极限并且远远不够的,因此我们在加载脚本文件时应该追求无阻塞的加载,比如:
- 对于不会修改DOM的脚本使用script标签的defer(HTML 4)或者async(HTML 5)属性来延迟执行。这两个属性相同点在于都是采用并行下载,不回造成阻塞,但是执行的时机不同,async是加载完成后自动执行,而defer需要等待页面完成后执行,详细的可以参考这里:https://www.w3.org/TR/html5/single-page.html#attr-script-defer
- 使用动态创建的script标签来下载和执行代码。也就是使用document.createElement来创建script标签,注意通常来说新创建的script标签添加到head标签里比body里更保险,尤其是在页面加载还未完成的时候执行这样的操作。
- 使用XHR对象下载js代码并注入页面中。类似第二点,但通过ajax来异步下载脚本文件。
简化css选择器和优化js的获取DOM方法
css选择器根据优先级是具有不同的效率的,通常id选择器较快,而通用、伪类、属性等选择器就较慢了,因此在编写css代码的时候出了减少嵌套,也需要合理的使用选择器。同样的,在js中,原生的通过id获取,或者jQuery中使用id选择$('#id')执行速度都会相当快,而其他的方法则要略逊一筹。另外值得一提的是,在使用jQuery时应该尽可能少的生成jQuery对象,因为生成这样一个带有需要方法和属性的对象本身就需要消耗的很多的资源,而且很多jQuery方法都有一个供jQuery函数使用的方法,举个例子:
// not good var $text = $('#text'); var $ts = $text.text(); //good var $text = $('#text'); var $ts = $.text($text);
后一种方法不通过jQuery对象操作,相对开销就比较小,速度会比较快。
关于jQuery的部分可以参考阮一峰老师的博客:jQuery设计思想和jQuery最佳实践
合并修改样式
考虑这个例子:
var el = document.getElementById('mydiv'); el.style.borderLeft = '1px'; el.style.borderRight = '2px'; el.style.padding = '5px';
在最糟糕的情况下,这三次样式属性的修改可能会导致浏览器发生三次重排,当然如今的浏览器都会对此作出优化,只会触发一次重排,但是在旧的浏览器中依然效率低下,而且这段代码只有四行,但是访问了四次DOM,因此我们可以对此作出优化,合并所有的改变并且一次访问就能实现显然是效率更高的做法,我们有两种方法可以去实现,第一种就是使用cssText属性:
var el = document.getElementById('mydiv'); // 修改并覆盖了已经存在的样式信息 el.style.cssText = 'border-left: 1px;border-right: 2px;padding: 5px;'; //注意如果你需要保留现有的样式,你可以把它附件在cssText字符串之后: el.style.cssText += 'border-left: 1px;border-right: 2px;padding: 5px;';
另外一种就是通过绑定class名而不是修改内联样式,这种方法适用于那些不依赖计算的固定样式变化,在逻辑上更清晰,也更易于维护:
var el = document.getElementById('mydiv'); el.className = 'active';
批量修改DOM
当我们需要对DOM元素进行一系列操作时,可以通过下面三个步骤来减少重绘和重排:
- 使元素脱离文档流。
- 对其应用多重改变。
- 把元素放回文档流。
这样我们只在第一步和第三步触发了重排,不这么做的话,你会在第二步的每一次修改可能都会触发一次重排。
要让元素脱离文档流,我们一般又有三种方法:
隐藏元素,应用修改,显示元素。
即 el.style.display='none',修改,应用,el.style.display='block'
使用文档片段(document fragment)在当前DOM外构建一个子树,再把它拷贝回文档。
即通过createDocumentFragment创建一个fragment,对其进行修改然后再利用appendChild等api使其加入文档中,
把原始元素拷贝到一个脱离文档的节点中,修改副本,然后再替换原始元素。
即通过cloneNode,对复制的node来修改,然后使用replaceChild来替换。
缓存信息
创建一个局部变量来保存信息而减少浏览器对此信息的查询次数。举个例子,在一个setTimeout的循环中,我们需要把一个元素按对角线从0,0,移动到500,500的位置:
// bad el.style.left = 1 + el.offsetLeft + 'px'; el.style.top = 1 + el.offsetTop + 'px'; if (el.offsetLeft >= 500) { stopAnimation(); } // good var current = el.offsetLeft; current++; el.style.left = current + 'px'; el.style.top = current + 'px'; if (current >= 500) { stopAnimation(); }
第二种方法浏览器不需要每次都去查询偏移量而可以直接使用一个变量,效率会大大提高。
处理动画时使元素脱离文档流
重排发生时渲染树中需要重新计算的节点越多,性能越低效,情况也越糟糕,因此在页面顶部的一个动画效果发生的时候如果推移了整个页面余下的部分时,可能会导致一次代价昂贵的大规模重排,让用户感觉页面一顿一顿的,我们可以按照以下步骤来避免页面其余部分的重拍:
- 使用绝对位置来定位页面上的动画元素,使其脱离文档流。
- 让元素动起来,它扩大时可能会覆盖部分页面但这只是页面一小部分的重绘,不回产生重排并重绘页面的大部分内容。
- 当动画结束后恢复定位,从而只会下移一次文档的其他元素。
减少hover
由于从IE7开始任何元素都可以使用:hover这个伪类,然而如果有大量元素使用时,会降低响应速度,这一点尤其在表格中表现的更为明显。比如有一个几百行的表格,使用了tr:hover来改变背景色,那么鼠标在表格上移动时,性能会明显降低,所以在元素很多的时候应该避免使用这种效果。
事件委托
关于事件流可以参考我之前的博客,在大量元素都需要绑定事件的时候,如果直接给每一个元素都绑定事件,这个代价往往是非常昂贵的。事件绑定占用了处理事件,而且浏览器需要跟踪每一个事件处理器,这也占用了更多的内存。而事件委托就是一个简单而优雅的方式来处理DOM事件。它的原理是很简单的:事件逐层冒泡并能被父元素捕获,因此只需要给一个上层元素绑定一个事件处理器,内部通过对目标元素的判断来实现响应子元素上触发的事件。比如一个列表中的每一个li都有一个点击事件,我们只需要给最外层的ul标签绑定一个点击监听就可以了。它的步骤是非常简单的:
- 访问事件对象,判断事件源头(target或者srcElement);
- 处理事件
- 取消文档树中的冒泡(可选)
- 阻止默认动作(可选)
选择作用域链最短的方法
我们知道js采用链式作用域,读取一个变量的时候会先在当前作用域寻找该变量,如果找不到的话,就往上一层作用域去寻找。这样的设计之下,很明显读取局部变量就要比去读取上一级的全局变量快很多。同理,在调用对象方法的时候,使用prototype也要比闭包模式慢:
// prototype var X = function(name){ this.name = name; } X.prototype.get_name = function() { return this.name; }; // closure var Y = function(name) { var y = { name: name }; return { 'get_name': function() { return y.name; } }; };
小结:
- 最小化DOM访问次数,如果需要多次访问某个DOM节点,使用变量来保存引用。
- 使用速度快的api,比如getElementById。
- 小心处理HTML集合,需要迭代操作时把长度缓存到变量中。
- 留意重绘和重排,批量修改样式,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数。
- 动画中使用绝对定位,使用拖放代理。
- 使用事件委托来减少事件处理的次数。