使用cloneNode在外部更新节点然后再通过replace与原始节点互换
var orig = document.getElementById('container'); var clone = orig.cloneNode(true); var list = ['foo', 'bar', 'baz']; var content; for (var i = 0; i < list.length; i++) { content = document.createTextNode(list[i]); clone.appendChild(content); } orig.parentNode.replaceChild(clone, orig);
多个节点插入操作,即使在外面设置节点的元素和风格再插入,由于多个节点还是会引发多次reflow
优化的方法是创建DocumentFragment,在其中插入节点后再添加到页面。 如JQuery中所有的添加节点的操作如append,都是最终调用DocumentFragment来实现的
createSafeFragment(document) { var list = nodeNames.split( "|" ), safeFrag = document.createDocumentFragment(); if (safeFrag.createElement) { while (list.length) { safeFrag.createElement( list.pop(); ); }; }; return safeFrag; };
如果需要动态更改CSS样式,尽量采用触发reflow次数较少的方式
如以下代码逐条更改元素的几何属性,理论上会触发多次reflow
element.style.fontWeight = 'bold' ; element.style.marginLeft= '30px' ; element.style.marginRight = '30px' ;
可以通过直接设置元素的className直接设置,只会触发一次reflow
element.className = 'selectedAnchor' ;
在console中执行命令查看DOM元素数量
document.getElementsByTagName( '*' ).length
正常页面的DOM元素数量一般不应该超过1000
DOM元素过多会使DOM元素查询效率,样式表匹配效率降低,是页面性能最主要的瓶颈之一
在JAVASCRIPT中,DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分
对于小的DOM更改,两者效率差不多,但对于大的DOM更改,innerHTML要比标准的DOM方法创建同样的DOM结构快得多
在对当前DOM进行操作之前,尽可能多的做一些准备工作,保证N次创建,1次写入
在对DOM操作之前,把要操作的元素,先从当前DOM结构中删除:
每次修改元素的style属性都会触发回流操作
element.style.backgroundColor = "blue";
将获取的DOM数据缓存起来。这种方法,对获取那些会触发回流操作的属性(比如offsetWidth等)尤为重要
当对HTMLCollection对象进行操作时,应该将访问的次数尽可能的降至最低,最简单的,你可以将length属性缓存在一个本地变量中,这样就能大幅度的提高循环的效率
JAVASCRIPT是个无类型的语言,这导致了如x=y+z这种表达式可以有很多含义
而JS引擎内部则使用“细粒度”的类型,比如:
这就要求js类型-js引擎类型,需要做“boxed/unboxed(装箱/解箱)”,在处理一次x=y+z这种计算,需要经过的步骤如下:
只有第5步骤是真正有效的操作,其他步骤都是为第5步骤做准备/收尾,JAVASCRIPT的untyped特性很好用,但也为此付出了很大的性能代价。
先看看JIT对untyped的优化,在JIT下,执行x=y+z流程
其中1,2步骤由CPU负责,7步骤JIT把结果保存在寄存器里。但可惜不是所有情况都能使用JIT,当number+number,string+string 等等可以使用JIT,但特殊情况,如:number+undefined就不行了,只能走旧解析器。
新引擎还对“对象属性”访问做了优化,解决方案叫inline caching,简称:IC。简单的说,就是做cache。但如果当list很大时,这种方案反而影响效率。
Type-specializing JIT引擎用来处理typed类型(声明类型)变量,但JAVASCRIPT都是untype类型的
Type-specializing JIT的解决方案是:
Type-specializing JIT的执行x=y+z流程:
代价是:
所以·Type-specializing JIT·的应用是有选择性,选择使用这个引擎的场景包括:
另外,有2点也需要注意:
借助LAB.js(装入和阻止JavaScript),你就可以并行装入JavaScript文件,加快总的装入过程。此外,你还可以为需要装入的脚本设置某个顺序,那样就能确保依赖关系的完整性。此外,开发者声称其网站上的速度提升了2倍。
现在许多网页使用内容分发网络(CDN)。它可以改进你的缓存机制,因为每个人都可以使用它。它还能为你节省一些带宽。你很容易使用ping检测或使用Firebug调试那些服务器,以便搞清可以从哪些方面加快数据的速度。选择CDN时,要照顾到你网站那些访客的位置。记得尽可能使用公共存储库。
也可以在头部分放置需要装入的一些JavaScript,但是前提是它以异步方式装入。
脚本加载与解析会阻塞HTML渲染,可以通过异步加载方式来避免渲染阻塞,步加载的方式很多,比较通用的方法如下。
var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/JavaScript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })();
或者
function loadjs (script_filename){ var script = document.createElement( 'script' ); script.setAttribute( 'type' , 'text/javascript' ); script.setAttribute( 'src' , script_filename); script.setAttribute( 'id' , 'script-id' ); scriptElement = document.getElementById( 'script-id' ); if (scriptElement){ document.getElementsByTagName( 'head' )[0].removeChild(scriptElement); } document.getElementsByTagName( 'head' )[0].appendChild(script); } var script = 'scripts/alert.js' ; loadjs(script);
将JavaScript/css数据打包成PNG文件。之后进行拆包,只要使用画布API的getImageData()。可以在不缩小数据的情况下,多压缩35%左右。而且是无损压缩,对比较庞大的脚本来说,在图片指向画布、读取像素的过程中,你会觉得有“一段”装入时间。
通过Cache-Control和Expires头可以将脚本文件缓存在客户端或者代理服务器上,可以减少脚本下载的时间。
Expires格式:
Expires = "Expires" ":" HTTP-date Expires: Thu, 01 Dec 1994 16:00:00 GMT Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the Expires field.
Cache-Control格式:
Cache-Control = "Cache-Control" ":" 1#cache-directive Cache-Control: public
具体的标准定义可以参考http1.1中的定义,简单来说Expires控制过期时间是多久,Cache-Control控制什么地方可以缓存 。
var person = { name: “Nicholas", age: 30 } function displayInfo() { var count = 5; with (person) { alert(name + ' is ' + age); alert( 'count is ' + count); } }
在业务代码中,一个变量已经确定不再需要了,那么就可以手工解除变量引用,以使其被回收。
var data = { /* some big data */ }; // ... data = null;
具有相同作用域变量通过一个var声明
jQuery.extend = jQuery.fn.extend = function () { var options, name, src, copy, copyIsArray, clone,target = arguments[0] || {}, i = 1, length = arguments.length, deep = false ; }
该项优化在IE上体现比较明显
var docElem = window.document.documentElement, selector_hasDuplicate, matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector ||docElem.msMatchesSelector, selector_sortOrder = function ( a, b ) { // Flag for duplicate removal if ( a === b ) { selector_hasDuplicate = true ; return 0; } }
除了使用闭包进行内部变量访问,我们还可以使用现在十分流行的回调函数来进行业务处理。
function getData(callback) { var data = 'some big data'; callback(null, data); } getData(function(err, data) { console.log(data); });
回调函数是一种后续传递风格(Continuation Passing Style, CPS)的技术,这种风格的程序编写将函数的业务重点从返回值转移到回调函数中去。而且其相比闭包的好处也有很多。
一些方法例如setTimeout()、setInterval(),接受字符串或者方法实例作为参数。直接传递方法对象作为参数来避免对字符串的二次解析。
传递方法
setTimeout(test, 1);
传递方法字符串
setTimeout('test()', 1);
方法调用一般封装了原始操作,在性能要求高的逻辑中,可以使用原始操作代替方法调用来提高性能。
原始操作
var min = a<b?a:b;
方法实例
var min = Math.min(a, b);
如果针对的是不断运行的代码,不应该使用setTimeout,而应该是用setInterval。setTimeout每次要重新设置一个定时器。
当JAVASCRIPT代码想解析JAVASCRIPT代码时就会存在双重解释惩罚,双重解释一般在使用eval函数、new Function构造函数和setTimeout传一个字符串时等情况下会遇到,如
eval("alert('hello world');"); var sayHi = new Function("alert('hello world');"); setTimeout("alert('hello world');", 100);
上述alert(‘hello world’);语句包含在字符串中,即在JS代码运行的同时必须新启运一个解析器来解析新的代码,而实例化一个新的解析器有很大的性能损耗。
我们看看下面的例子:
var sum, num1 = 1, num2 = 2; /**效率低**/ for(var i = 0; i < 10000; i++){ var func = new Function("sum+=num1;num1+=num2;num2++;"); func(); //eval("sum+=num1;num1+=num2;num2++;"); } /**效率高**/ for(var i = 0; i < 10000; i++){ sum+=num1; num1+=num2; num2++; }
第一种情况我们是使用了new Function来进行双重解释,而第二种是避免了双重解释。
只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。
JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式
/**不提倡**/ var i = 1; var j = "hello"; var arr = [1,2,3]; var now = new Date(); /**提倡**/ var i = 1, j = "hello", arr = [1,2,3], now = new Date();
/**不提倡**/ var name = values[i]; i++; /**提倡**/ var name = values[i++];
/**不提倡**/ var a = new Array(); a[0] = 1; a[1] = "hello"; a[2] = 45; var o = new Obejct(); o.name = "bill"; o.age = 13; /**提倡**/ var a = [1, "hello", 45]; var o = { name : "bill", age : 13 };
JavaScript不需要属性访问方法,因为所有的属性都是外部可见的。
添加属性访问方法只是增加了一层重定向 ,对于访问控制没有意义。
function Car() { this .m_tireSize = 17; this .m_maxSpeed = 250; this .GetTireSize = Car_get_tireSize; this .SetTireSize = Car_put_tireSize; } function Car_get_tireSize() { return this .m_tireSize; } function Car_put_tireSize(value) { this .m_tireSize = value; } var ooCar = new Car(); var iTireSize = ooCar.GetTireSize(); ooCar.SetTireSize(iTireSize + 1);
function Car() { this .m_tireSize = 17; this .m_maxSpeed = 250; } var perfCar = new Car(); var iTireSize = perfCar.m_tireSize; perfCar.m_tireSize = iTireSize + 1;
一般浏览器都会使用增量reflow的方式将需要reflow的操作积累到一定程度然后再一起触发,但是如果脚本中要获取以下属性,那么积累的reflow将会马上执行,已得到准确的位置信息。
offsetLeft offsetTop offsetHeight offsetWidth scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle()
精简代码就是将代码中的空格和注释去除,也有更进一步的会对变量名称混淆、精简。根据统计精简后文件大小会平均减少21%,即使Gzip之后文件也会减少5%。
JS与其他语言不同在于它的执行效率很大程度是取决于JS engine的效率。除了引擎实现的优劣外,引擎自己也会为一些特殊的代码模式采取一些优化的策略。例如FF、Opera和Safari的JAVASCRIPT引擎,都对字符串的拼接运算(+)做了特别优化。所以应该根据不同引擎进行不同优化。
而如果做跨浏览器的web编程,则最大的问题是在于IE6(JScript 5.6),因为在不打hotfix的情况下,JScript引擎的垃圾回收的bug,会导致其在真实应用中的performance跟其他浏览器根本不在一个数量级上。因此在这种场合做优化,实际上就是为JScript做优化,所以第一原则就是只需要为IE6(未打补丁的JScript 5.6或更早版本)做优化。
JS优化总是出现在大规模循环的地方:
这倒不是说循环本身有性能问题,而是循环会迅速放大可能存在的性能问题,所以第二原则就是以大规模循环体为最主要优化对象。
以下的优化原则,只在大规模循环中才有意义,在循环体之外做此类优化基本上是没有意义的。
目前绝大多数JS引擎都是解释执行的,而解释执行的情况下,在所有操作中,函数调用的效率是较低的。此外,过深的prototype继承链或者多级引用也会降低效率。JScript中,10级引用的开销大体是一次空函数调用开销的1/2。这两者的开销都远远大于简单操作(如四则运算)。
尽量避免过多的引用层级和不必要的多次方法调用:
特别要注意的是,有些情况下看似是属性访问,实际上是方法调用。例如所有DOM的属性,实际上都是方法。在遍历一个NodeList的时候,循环 条件对于nodes.length的访问,看似属性读取,实际上是等价于函数调用的。而且IE DOM的实现上,childNodes.length每次是要通过内部遍历重新计数的。(My god,但是这是真的!因为我测过,childNodes.length的访问时间与childNodes.length的值成正比!)这非常耗费。所以 预先把nodes.length保存到js变量,当然可以提高遍历的性能。
同样是函数调用,用户自定义函数的效率又远远低于语言内建函数,因为后者是对引擎本地方法的包装,而引擎通常是c,c++,java写的。进一步,同样的功能,语言内建构造的开销通常又比内建函数调用要效率高,因为前者在JS代码的parse阶段就可以确定和优化。
尽量使用语言本身的构造和内建函数:
这里有一个例子是高性能的String.format方法。 String.format传统的实现方式是用String.replace(regex, func),在pattern包含n个占位符(包括重复的)时,自定义函数func就被调用n次。而这个高性能实现中,每次format调用所作的只是一次Array.join然后一次String.replace(regex, string)的操作,两者都是引擎内建方法,而不会有任何自定义函数调用。两次内建方法调用和n次的自定义方法调用,这就是性能上的差别。
同样是内建特性,性能上也还是有差别的。例如在JScript中对于arguments的访问性能就很差,几乎赶上一次函数调用了。因此如果一个 可变参数的简单函数成为性能瓶颈的时候,可以将其内部做一些改变,不要访问arguments,而是通过对参数的显式判断来处理,比如:
function sum() { var r = 0; for (var i = 0; i < arguments.length; i++) { r += arguments[i]; } return r; }
这个sum通常调用的时候个数是较少的,我们希望改进它在参数较少时的性能。如果改成:
function sum() { switch (arguments.length) { case 1: return arguments[0]; case 2: return arguments[0] + arguments[1]; case 3: return arguments[0] + arguments[1] + arguments[2]; case 4: return arguments[0] + arguments[1] + arguments[2] + arguments[3]; default: var r = 0; for (var i = 0; i < arguments.length; i++) { r += arguments[i]; } return r; } }
其实并不会有多少提高,但是如果改成:
function sum(a, b, c, d, e, f, g) { var r = a ? b ? c ? d ? e ? f ? a + b + c + d + e + f : a + b + c + d + e : a + b + c + d : a + b + c : a + b : a : 0; if (g === undefined) return r; for (var i = 6; i < arguments.length; i++) { r += arguments[i]; } return r; }
就会提高很多(至少快1倍)。
动画效果在缺少硬件加速支持的情况下反应缓慢,例如手机客户端。
特效应该只在确实能改善用户体验时才使用,而不应用于炫耀或者弥补功能与可用性上的缺陷。
至少要给用户一个选择可以禁用动画效果。
设置动画元素为absolute或fixed。
使用一个timer完成多个元素动画。
动画效果的帧率最优化的情况是使用一个timer完成多个对象的动画效果,其原因在于多个timer的调用本身就会损耗一定性能。
setInterval(function() { animateFirst(''); }, 10); setInterval(function() { animateSecond(''); }, 10);
使用同一个timer
setInterval(function() { animateFirst(''); animateSecond(''); }, 10);
以脚本为基础的动画,由浏览器控制动画的更新频率。
访问对象属性消耗性能过程(JAVASCRIPT对象存储)。
function f(obj) { return obj.a + 1; }
更改404错误响应页面可以改进用户体验,但是同样也会浪费服务器资源。
指向外部JAVASCRIPT的链接出现问题并返回404代码。
重复调用脚本缺点
ETags用来判断浏览器缓存里的元素是否和原来服务器上的一致。
如某个文件在1秒内修改了10次,ETags可以综合Inode(文件的索引节点inode数),MTime(修改时间)和Size来精准的进行判断,避开UNIX记录MTime只能精确到秒的问题。服务器集群使用,可取后两个参数。使用ETags减少Web应用带宽和负载
减少主机名可以节省响应时间。但同时也会减少页面中并行下载的数量。
应用””+1,效率是最高。
性能上来说:””+字符串>String()>.toString()>new String()。
switch语句
若有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。
以Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。
另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。
新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等
老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
垃圾回收算法一直是编程语言的研发中是否重要的一环,而V8引擎所使用的垃圾回收算法主要有以下几种。
引用是指代码对对象的访问这一抽象关系,它与C/C++的指针有点相似,但并非同物。引用同时也是JAVASCRIPT引擎在进行垃圾回收中最关键的一个机制。
var val = 'hello world'; function foo() { return function() { return val; }; } global.bar = foo();
当代码执行完毕时,对象val和bar()并没有被回收释放,JAVASCRIPT代码中,每个变量作为单独一行而不做任何操作,JAVASCRIPT引擎都会认为这是对对象的访问行为,存在了对对象的引用。为了保证垃圾回收的行为不影响程序逻辑的运行,JAVASCRIPT引擎不会把正在使用的对象进行回收。所以判断对象是否正在使用中的标准,就是是否仍然存在对该对象的引用。
-
JAVASCRIPT
的引用
是可以进行转移
的,那么就有可能出现某些引用被带到了全局作用域,但事实上在业务逻辑里已经不需要对其进行访问了,这个时候就应该被回收,但是JAVASCRIPT
引擎仍会认为程序仍然需要它。
给DOM对象添加的属性是一个对象的引用
var MyObject = {}; document.getElementByIdx_x('myDiv').myProp = MyObject;
解决方法:在window.onunload事件中写上:
document.getElementByIdx_x('myDiv').myProp = null;
DOM对象与JS对象相互引用
function Encapsulator(element) { this.elementReference = element; element.myProp = this; } new Encapsulator(document.getElementByIdx_x('myDiv'));
解决方法:在onunload事件中写上:
document.getElementByIdx_x('myDiv').myProp = null;
给DOM对象用attachEvent绑定事件
function doClick() {} element.attachEvent("onclick", doClick);
解决方法:在onunload事件中写上:
element.detachEvent('onclick', doClick);
从外到内执行appendChild。这时即使调用removeChild也无法释放。
var parentDiv = document.createElement_x("div"); var childDiv = document.createElement_x("div"); document.body.appendChild(parentDiv); parentDiv.appendChild(childDiv);
解决方法:从内到外执行appendChild:
var parentDiv = document.createElement_x("div"); var childDiv = document.createElement_x("div"); parentDiv.appendChild(childDiv); document.body.appendChild(parentDiv);
反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。
for(i = 0; i < 5000; i++) { hostElement.text = "asdfasdfasdf"; }
这种方式相当于定义了5000个属性,解决方法:无。
CollectGarbage是IE的一个特有属性,用于释放内存的使用方法,将该变量或引用对象设置为null或delete然后在进行释放动作,在做CollectGarbage前,要必需清楚的两个必备条件:(引用)。
捕获型事件先发生。两种事件流会触发DOM中的所有对象,从document对象开始,也在document对象结束。
<ul id="parent-list"> <li id="post-1">Item 1 <li id="post-2">Item 2 <li id="post-3">Item 3 <li id="post-4">Item 4 <li id="post-5">Item 5 <li id="post-6">Item 6 </li></ul> // Get the element, add a click listener... document.getElementById("parent-list").addEventListener("click",function(e) { // e.target is the clicked element! // If it was a list item if(e.target && e.target.nodeName == "LI") { // List item found! Output the ID! console.log("List item ",e.target.id.replace("post-")," was clicked!"); } });
当需要使用数组时,可使用JSON格式的语法
如果需要遍历数组,应该先缓存数组长度,将数组长度放入局部变量中,避免多次查询数组长度。
在Blink/Webkit浏览器中(Chrome, Safari, Opera),我们可以借助其中的Developer Tools的Profiles工具来对我们的程序进行内存检查。
Developer Tools - Profiles
在Node.js中,我们可以使用node-heapdump和node-memwatch模块进行内存检查。
var heapdump = require('heapdump'); var fs = require('fs'); var path = require('path'); fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);
在业务代码中引入node-heapdump之后,我们需要在某个运行时期,向Node.js进程发送SIGUSR2信号,让node-heapdump抓拍一份堆内存的快照。
$ kill -USR2 (cat app.pid)
这样在文件目录下会有一个以heapdump-..heapsnapshot格式命名的快照文件,我们可以使用浏览器的Developer Tools中的Profiles工具将其打开,并进行检查。
循环是一种常用的流程控制
推荐使用for循环,如果循环变量递增或递减,不要单独对循环变量赋值,而应该使用嵌套的++或–-运算符。
代码的可读性对于for循环的优化。
用-=1。
从大到小的方式循环(这样缺点是降低代码的可读性)。
/**效率低**/ var divs = document.getElementsByTagName("div"); for(var i = 0; i < divs.length; i++){ ... } /**效率高,适用于获取DOM集合,如果纯数组则两种情况区别不到**/ var divs = document.getElementsByTagName("div"); for(var i = 0, len = divs.length; i < len; i++){ ... } /**在`IE6.0`下,`for(;;)`循环在执行中,第一种情况会每次都计算一下长度,而第二种情况却是在开始的时候计算长度,并把其保存到一个变量中,所以其执行效率要高点,所以在我们使用`for(;;)`循环的时候,特别是需要计算长度的情况,我们应该开始将其保存到一个变量中。**/
for(;;)、while()循环的性能基本持平。
在这三种循环中for(in)内部实现是构造一个所有元素的列表,包括array继承的属性,然后再开始循环,并且需要查询hasOwnProperty。所以for(in)相对for(;;)循环性能要慢。
对象上的任何属性查找都要比访问变量或数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,即属性查找越多,执行时间越长。所以针对需要多次用到对象属性,应将其存储在局部变量。
优化1:简化终止条件
for(var i = 0, len = values.length; i < len; i++) { process(values[i]); }
优化2:使用后测试循环(注意:使用后测试循环需要确保要处理的值至少有一个)
var i values.length - 1; if(i -1) { do { process(values[i]); }while(--i >= 0); }
展开循环
// Jeff Greenberg for JS implementation of Duff's Device // 假设:values.length 0 function process(v) { alert(v); } var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; var iterations = Math.ceil(values.length / 8); var startAt = values.length % 8; var i = 0; do { switch(startAt) { case 0 : process(values[i++]); case 7 : process(values[i++]); case 6 : process(values[i++]); case 5 : process(values[i++]); case 4 : process(values[i++]); case 3 : process(values[i++]); case 2 : process(values[i++]); case 1 : process(values[i++]); } startAt = 0; }while(--iterations 0);
如上展开循环可以提升大数据集的处理速度。接下来给出更快的Duff装置技术,将do-while循环分成2个单独的循环。(注:这种方法几乎比原始的Duff装置实现快上40%。)
// Speed Up Your Site(New Riders, 2003) function process(v) { alert(v); } var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]; var iterations = Math.floor(values.length / 8); var leftover = values.length % 8; var i = 0; if(leftover 0) { do { process(values[i++]); }while(--leftover 0); } do { process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); }while(--iterations 0);
针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。
循环中使用try-catch
for ( var i = 0; i < 200; i++) { try {} catch (e) {} }
循环外使用try-catch
try { for ( var i = 0; i < 200; i++) {} } catch (e) {}
避免遍历大量元素:
避免对全局DOM元素进行遍历,如果parent已知可以指定parent在特定范围查询。
var elements = document.getElementsByTagName( '*' ); for (i = 0; i < elements.length; i++) { if (elements[i].hasAttribute( 'selected' )) {} }
如果已知元素存在于一个较小的范围内,
var elements = document.getElementById( 'canvas' ).getElementsByTagName ( '*' ); for (i = 0; i < elements.length; i++) { if (elements[i].hasAttribute( 'selected' )) {} }
通过原型优化方法定义。
使用运算符时,尽量使用+=,-=、*=、\=等运算符号,而不是直接进行赋值运算
位运算
减少页面重绘虽然本质不是JAVASCRIPT优化,但重绘往往是由JAVASCRIPT引起的,而重绘的情况直接影响页面性能。
var str = "<div>这是一个测试字符串</div>"; /**效率低**/ var obj = document.getElementsByTagName("body"); for(var i = 0; i < 100; i++){ obj.innerHTML += str + i; } /**效率高**/ var obj = document.getElementsByTagName("body"); var arr = []; for(var i = 0; i < 100; i++){ arr[i] = str + i; } obj.innerHTML = arr.join("");
一般影响页面重绘的不仅仅是innerHTML,如果改变元素的样式,位置等情况都会触发页面重绘,所以在平时一定要注意这点。
替换、查找等操作,使用正则表达式
因为JAVASCRIPT的循环速度较慢,而正则表达式的操作是用C写成的API,性能比较好。
字符串的拼接在我们开发中会经常遇到,所以我把其放在首位,我们往往习惯的直接用+=的方式来拼接字符串,其实这种拼接的方式效率非常的低,我们可以用一种巧妙的方法来实现字符串的拼接,那就是利用数组的join方法,具体请看我整理的:Web前端开发规范文档中的javaScript书写规范倒数第三条目。
不过也有另一种说法,通常认为需要用Array.join的方式,但是由于SpiderMonkey等引擎对字符串的“+”运算做了优化,结果使用Array.join的效率反而不如直接用“+”,但是如果考虑IE6,则其他浏览器上的这种效率的差别根本不值一提。具体怎么取舍,诸君自定。
作用域(scope)是JAVASCRIPT编程中一个重要的运行机制,在JAVASCRIPT同步和异步编程以及JAVASCRIPT内存管理中起着至关重要的作用。
在JAVASCRIPT中,能形成作用域的有如下几点。
以下代码为例:
var foo = function() { var local = {}; }; foo(); console.log(local); //=undefined var bar = function() { local = {}; }; bar(); console.log(local); //={} /**这里我们定义了foo()函数和bar()函数,他们的意图都是为了定义一个名为local的变量。在foo()函数中,我们使用var语句来声明定义了一个local变量,而因为函数体内部会形成一个作用域,所以这个变量便被定义到该作用域中。而且foo()函数体内并没有做任何作用域延伸的处理,所以在该函数执行完毕后,这个local变量也随之被销毁。而在外层作用域中则无法访问到该变量。而在bar()函数内,local变量并没有使用var语句进行声明,取而代之的是直接把local作为全局变量来定义。故外层作用域可以访问到这个变量。**/ local = {}; // 这里的定义等效于 global.local = {};
在JAVASCRIPT编程中,会遇到多层函数嵌套的场景,这就是典型的作用域链的表示。
function foo() { var val = 'hello'; function bar() { function baz() { global.val = 'world;' }; baz(); console.log(val); //=hello }; bar(); }; foo(); /**在`JAVASCRIPT`中,变量标识符的查找是从当前作用域开始向外查找,直到全局作用域为止。所以`JAVASCRIPT`代码中对变量的访问只能向外进行,而不能逆而行之。baz()函数的执行在全局作用域中定义了一个全局变量val。而在bar()函数中,对val这一标识符进行访问时,按照从内到外的查找原则:在bar函数的作用域中没有找到,便到上一层,即foo()函数的作用域中查找。然而,使大家产生疑惑的关键就在这里:本次标识符访问在foo()函数的作用域中找到了符合的变量,便不会继续向外查找,故在baz()函数中定义的全局变量val并没有在本次变量访问中产生影响。**/
JAVASCRIPT代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要遍历当前执行环境的作用域链,而遍历是从这个作用域链的前端一级一级的向后遍历,直到全局执行环境。
/**效率低**/ for(var i = 0; i < 10000; i++){ var but1 = document.getElementById("but1"); } /**效率高**/ /**避免全局查找**/ var doc = document; for(var i = 0; i < 10000; i++){ var but1 = doc.getElementById("but1"); } /**上面代码中,第二种情况是先把全局对象的变量放到函数里面先保存下来,然后直接访问这个变量,而第一种情况是每次都遍历作用域链,直到全局环境,我们看到第二种情况实际上只遍历了一次,而第一种情况却是每次都遍历了,而且这种差别在多级作用域链和多个全局变量的情况下还会表现的非常明显。在作用域链查找的次数是`O(n)`。通过创建一个指向`document`的局部变量,就可以通过限制一次全局查找来改进这个函数的性能。**/
JAVASCRIPT中的标识符查找遵循从内到外的原则。
function foo() { var local = 'Hello'; return function() { return local; }; } var bar = foo(); console.log(bar()); //=Hello /**这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到`延伸`。foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。**/
闭包是JAVASCRIPT的高级特性,因为把带有内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。
良好的闭包管理。
循环事件绑定、私有属性、含参回调等一定要使用闭包时,并谨慎对待其中的细节。
循环绑定事件,我们假设一个场景:有六个按钮,分别对应六种事件,当用户点击按钮时,在指定的地方输出相应的事件。
var btns = document.querySelectorAll('.btn'); // 6 elements var output = document.querySelector('#output'); var events = [1, 2, 3, 4, 5, 6]; // Case 1 for (var i = 0; i < btns.length; i++) { btns[i].onclick = function(evt) { output.innerText += 'Clicked ' + events[i]; }; } /**这里第一个解决方案显然是典型的循环绑定事件错误,这里不细说,详细可以参照我给一个网友的回答;而第二和第三个方案的区别就在于闭包传入的参数。**/ // Case 2 for (var i = 0; i < btns.length; i++) { btns[i].onclick = (function(index) { return function(evt) { output.innerText += 'Clicked ' + events[index]; }; })(i); } /**第二个方案传入的参数是当前循环下标,而后者是直接传入相应的事件对象。事实上,后者更适合在大量数据应用的时候,因为在JavaScript的函数式编程中,函数调用时传入的参数是基本类型对象,那么在函数体内得到的形参会是一个复制值,这样这个值就被当作一个局部变量定义在函数体的作用域内,在完成事件绑定之后就可以对events变量进行手工解除引用,以减轻外层作用域中的内存占用了。而且当某个元素被删除时,相应的事件监听函数、事件对象、闭包函数也随之被销毁回收。**/ // Case 3 for (var i = 0; i < btns.length; i++) { btns[i].onclick = (function(event) { return function(evt) { output.innerText += 'Clicked ' + event; }; })(events[i]); }
闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。
闭包的性能不如使用内部方法,更不如重用外部方法。
由于IE 9浏览器的DOM节点作为COM对象来实现,COM的内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦DOM引用了闭包(例如event handler),闭包的上层元素又引用了这个DOM,就会造成循环引用从而导致内存泄漏。
使用一个匿名函数在代码的最外层进行包裹。
;(function() { // 主业务代码 })();
有的甚至更高级一点:
;(function(win, doc, $, undefined) { // 主业务代码 })(window, document, jQuery);
甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:
/**RequireJS**/ define(['jquery'], function($) { // 主业务代码 }); /**SeaJS**/ define('module', ['dep', 'underscore'], function($, _) { // 主业务代码 });
被定义在全局作用域的对象,可能是会一直存活到进程退出的,如果是一个很大的对象,那就麻烦了。比如有的人喜欢在JavaScript中做模版渲染:
<?php $db = mysqli_connect(server, user, password, 'myapp'); $topics = mysqli_query($db, "SELECT * FROM topics;"); ?> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>你是猴子请来的逗比么?</title> </head> <body> <ul id="topics"></ul> <script type="text/tmpl" id="topic-tmpl"> <li class="topic"> <h1><%=title%></h1> <p><%=content%></p> </li> </script> <script type="text/javascript"> var data = <?php echo json_encode($topics); ?>; var topicTmpl = document.querySelector('#topic-tmpl').innerHTML; var render = function(tmlp, view) { var complied = tmlp .replace(/\n/g, '\\n') .replace(/<%=([\s\S]+?)%>/g, function(match, code) { return '" + escape(' + code + ') + "'; }); complied = [ 'var res = "";', 'with (view || {}) {', 'res = "' + complied + '";', '}', 'return res;' ].join('\n'); var fn = new Function('view', complied); return fn(view); }; var topics = document.querySelector('#topics'); function init() data.forEach(function(topic) { topics.innerHTML += render(topicTmpl, topic); }); } init(); </script> </body> </html>
在从数据库中获取到的数据的量是非常大的话,前端完成模板渲染以后,data变量便被闲置在一边。可因为这个变量是被定义在全局作用域中的,所以JAVASCRIPT引擎不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。可是如果我们作出一些很简单的修改,在逻辑代码外包装一层函数,这样效果就大不同了。当UI渲染完成之后,代码对data的引用也就随之解除,而在最外层函数执行完毕时,JAVASCRIPT引擎就开始对其中的对象进行检查,data也就可以随之被回收。
Github:前端性能优化指南
参考和借鉴了大家的经验,收集整理了这一篇开发规范,感谢所有的原作者,众人拾柴火焰高,技术无国界,持续更新中。
参考自:前端性能优化指南