前端代码性能优化
前端代码优化
前端标准html、js,查这里mozilla标准(w3c给的是纸面标准,这里是业界实际使用的标准)
developer.mozilla.org/zh-CN/
》作用域链越长,执行性能越差
当函数执行时,会形成自己的执行环境,执行环境会与函数的作用域链进行链接,并创建与之关联的活动对象(activation object)。执行环境中定义的所有变量和函数都保存在这个对象中。当函数执行完成后,这个对象会被系统销毁。
》避免函数内部多次逐级向上查找变量,使用局部变量存储一下
例如在函数开头var doc=document
》避免使用with
with关键字指定其内部未指明作用域的变量都使用它声明的作用域
with(document){
var bd=body; // 相当于使用document.body
}
带来的性能问题是,with会使作用域链变长,降低内部本地变量的查找(会先从with指定的作用域查找,然后再查找本地作用域),建议不要使用。
》减少DOM操作
浏览器执行分两个引擎:ECMAScript引擎和DOM引擎,前者执行js代码,非常快,后者操作UI,无论是读还是写代价都很高。例如Chrome使用V8引擎来驱动ECMAScript,使用Webkit中的WebCore来驱动DOM。
显著提高性能的策略就是增加JS操作,减少DOM操作。
》DOM操作优化
>注意获取DOM元素返回是类数组,其length是动态计算的
var alldivs = document.getElementsByTagName("div");
for (var i = 0; i < alldivs.length; i++){
document.body.appendChild(document.createElement("div"));
}
这会进入死循环,因为alldivs.length是动态的,建议的用本地变量存起来,包括查找出的结果,减少不避要的重复查找(例如若多次访问alldivs[i],可用变量存起来)
for (var i=0, len=divs.length; i<len; i++)
即:越怕麻烦越麻烦
》getElementById getElementsByTagName querySelectorAll
现代浏览器对querySelectorAll() 都做了优化,复杂语句的查询效率得到提升,最重要的,它能直接接受多条CSS语句。对于相对复杂的查找,getElementById getElementsByTagName的效率并没有querySelectorAll() 高。
var elements = document.querySelectorAll("#menu a");
var elements = document.getElementById("menu").getElementsByTagName("a");
》页面的重排与重绘
>DOM树和渲染树:
DOM树是整个页面的骨架,每个节点在渲染树上都至少有一个对应节点。隐藏的DOM树节点是没有对应的渲染树节点。
渲染树上的每一个节点都可称之为frame或者box,对应CSS的盒子模型,margin、 padding、border等。
当DOM树和渲染树都创建完成,页面便开始绘制(paint)。
>重排与重绘
如果DOM树的某些变化,导致某个元素的拓扑发生改变,例如边框变粗、加入文字等,那么浏览器就会重新计算该元素的拓扑结构,以及其它被影响到的元素的拓扑结构。同时浏览器会将这部分的拓扑改变,映射到渲染树上,对渲染树进行重新的构建,称之为重排(reflow)。
当重排完成之后,浏览器就会对这部分进行重新绘制,称之为重绘(repaint)。
并不是所有的DOM树改变都会影响拓扑结构。例如改变某个元素的背景色,就只会导致重绘,而不会导致重排。但重排一定会导致重绘。重排或者重绘都会大大消耗系统资源。
>导致重排的因素有:
一个可见的DOM元素被添加或者删除
元素改变的位置
元素的尺寸发生了变化(margin、padding、border、width、height等等)
内容发生了改变(例如文字、图片的增删改)
页面初始化加载
浏览器窗口发生了尺寸的改变
>导致马上重绘的因素有:
现代浏览器对重排与重绘操作,都做了很好的优化。即使频繁操作,浏览器也会进行相应的管理,构建队列等,最后再一次性进行重排或重绘。
但如果用户读取下面这些属性,浏览器为了获得正确的值,就会马上重绘页面,消耗大量资源:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle() (currentStyle in IE)
》减少重排与重绘
>避免DOM属性的读和写操作交叉
var computed, tmp = [], bodystyle = document.body.style;
if (document.body.currentStyle) { // IE, Opera
computed = document.body.currentStyle;
} else { // W3C
computed = document.defaultView.getComputedStyle(document.body, "");
}
bodystyle.color = "red"; // 写
tmp[0] = computed.backgroundColor; // 读
bodystyle.color = "white"; // 写
tmp[1] = computed.backgroundImage; // 读
bodystyle.color = "green";
tmp[2] = computed.backgroundAttachment;
上面的操作会导致三次马上重绘
bodystyle.color = "red";
bodystyle.color = "white";
bodystyle.color = "green";
tmp[0] = computed.backgroundColor;
tmp[1] = computed.backgroundImage;
tmp[2] = computed.backgroundAttachment;
这样优化后只重绘一次
var el = document.getElementById("mydiv");
el.style.borderLeft = "1px";
el.style.borderRight = "2px";
el.style.padding = "5px";
尽管浏览器会对这三次操作进行排队、合并,但还是应该采用合理的方式(始终记住js操作效率远高于dom操作):
el.style.cssText += "border-left:1px; border-right:2px; padding:5px;";
或者写成一个外部css样式
>充分利用DocumentFragment
在创建多个相同或者相似元素时,应该合理利用文档碎片DocumentFragment来处理,浏览器有专门的优化。例如给页面一次性添加一百万个<div>元素:
function createDivs(){
var tFrag = document.createDocumentFragment();
for (var i=0; i<1000000; i++){
tFrag.appendChild(document.createElement("div"));
}
document.body.appendChild(tFrag);
}
>让元素脱离文档流
显示或者隐藏页面的某个部分,是经常遇到的需求。尤其在制作动画的时候。重排或者重绘有时只会影响页面的某个区域,有时会影响整棵树。
当一个元素从页面顶部动画到底部时,可能会影响整个页面的DOM树,因此可以考虑将其移出文档流。
->将元素设置为绝对定位,让其脱离文档流。
->进行动画操作,只影响页面局部。
->再根据情况,重新修改position属性,让其回归文档流。
》使用事件委托
利用事件冒泡机制,将事件绑定到父元素,子元素的事件委托给父元素处理,避免子元素频繁的变化导致重复绑定。event delegation
var myul = sina.$("newslist");
var lis = myul.getElementsByTagName("li");
for(var i=0,l=lis.length;i<l;i++){
sina.addEvent(lis[i],"click",function(e){
e = e || window.event;
var target = e.target || e.srcElement;
alert(target.innerHTML);
});
}
可优化为:
var myul = sina.$("newslist");
sina.addEvent(myul,"click",function(e){
e = e || window.event;
var target = e.target || e.srcElement;
if(target.tagName.toLowerCase()=="li"){
alert(target.innerHTML);
}
});
典型应用:地图、Gmail、新闻列表翻页等等。
》一些编程技巧
for-in是四种循环里性能明显低于其它三种的,应尽量避免使用。只有在不知道对象属性数量、名称时才考虑;
减少对length属性的查询能够提高性能;
if-else与switch性能差不多,而lookup tables查表法对于键值对key-value类型的分支判断的有效;
空间换时间,例如把递归结果缓存起来直接使用
》字符串操作
+ += join可大胆使用,减少concat使用
充分利用数组的方法处理字符口串,例如反转字符串 str.split("").reverse().join("")足够高效和简洁
》正则表达式优化
所有浏览器执行正则表达式都很快,稍微复杂的字符串匹配,用正则校验会比自己写校验逻辑更高效。
一个正则表达式在匹配字符串的时候,经历了四个步骤:
编译、设置开始点、匹配每一个正则标识、成功或者失败,步骤3可能有回溯,步骤2-4可能重复执行
>编译
创建一个正则表达式时,浏览器会检查是否符合规则。然后将该正则编译到底层,用于之后的对比。因此正则的速度通常都较快,是在浏览器更底层来完成对比,能应对复杂查询。
var myReg = "/h(ello|appy) hippo/g"; /g表示全局匹配,或尽可能长匹配,/i表示不区分大小写
正则表达式的优化主要是在减少回溯次数
var myReg = "/h(ello|appy) hippo/g";
var tStr = "hello there, happy hippo";
->从字符串的第一个位置开始匹配,h匹配成功,字符串这个位置被记录。
->正则逐一往后走,ello 都匹配成功,但h与t匹配失败。此时正则回溯(backtrack),回溯到之前成功的位置h,试图匹配appy,匹配失败。
->字符串返回到之前记录的位置h后面的一个位置e,重新匹配整个正则。无法匹配h,下一个位置l,也失败。直到第14个字符,又是h,匹配成功。
->重复步骤2,ello 匹配失败,此时正则回溯(backtrack)到之前成功的位置h,试图匹配appy,成功匹配。继续往下 hippo匹配也成功。
整个匹配过程正则表达式有两次回溯,一次在字符串的第一个h,另一次在字符串的第二个h。
var str = "<p>Para 1.</p><img src=‘s.jpg‘><p>Para 2.</p><div>Div.</div>";
var reg1 = /<p>.*<\/p>/; //16次匹配出结果
var reg2 = /<p>.*?<\/p>/; //22次匹配出结果
对比贪婪模式与懒惰模式:
第一个正则表达式是贪婪匹配,在匹配<p>成功后,.* 会直接匹配到字符串的末端,然后往前逐一寻找<。
第二个正则表达式是懒惰匹配,在匹配<p>成功后,.*? 会从P字符开始,逐一往后寻找<。
每一次匹配不成功,正则表达式都会回溯到 .* 或 .*?,寻找下一个分支。.* 回溯后是减少一个字符,.*? 回溯后是增加一个字符。
每回退一个字符,整体匹配一下模式串看是否成功
由于 | 很容易导致回溯,应当尽可能的采用别的方式。
不要使用 使用
cat|bat [cb]at
red|read rea?d
red|raw r(?:ed|aw)
(.|\r|\n) [\s\S]
其他分析参见PPT详解
》Web Workers多线程
javascript是一门单线程的语言,在遇到复杂计算时,可能会阻断整个页面,HTML5通过workers的模式,在一定程度上给js开启了多线程的大门。
外部的Worker是不能操作主线程DOM树的,这样也能保证在执行子线程的时候,主线程的UI不会被打断。在子线程中,通过 self 对象来指向自己。与主线程的通讯方式也是通过 message,接收、计算完成后 post 回去。
》其他
js变量提升:只是声明提升,赋值动作不会,所以刚开始会是undefined
js原型链:每一个对象都有成员属性_prop_指向类,类本身也有属性_prop_指向父类(如object),对象通过this访问成员属性或方法且本身没有命中时,会自动从_porp_中搜索原型链。假设有类(或函数)Book和对象book1、book2,当给类添加方法,即Book.protype.sayHi = function (){},则对象book1、book2能访问到sayHi方法。