代码改变世界

JavaScript 优化

2013-04-19 18:22  二当家的  阅读(251)  评论(0编辑  收藏  举报

最近看完了《高性能JavaScript》一书,虽然以前写代码从来没注意这么多,但也是自己还没到那个地步........

既然看了,总要总结一下吧,好记性不如烂笔头,于是就有了这篇博客,全篇主要按原书章节目录来介绍下JS优化知识。

一、JS脚本的加载会阻塞页面

 解决方法:

  1. 将脚本放在body的尾部。
  2. 合并脚本,减少HTTP请求。
  3. 使用无阻塞方式加载脚本。无阻塞,即加载脚本的同时不会影响浏览器的其他进程。所以这里可以使用好几种方法来实现。
    • 延迟脚本。script标签具有一个defer的属性,加上此属性的script标签表明该脚本不修改DOM,因此,浏览器解析到并下载此脚本时并不会执行它,而是在DOM加载完成后,onload事件触发前执行。这类文件的下载不会阻塞浏览器的其他进程,可以与页面中其他资源并行下载。
    • 动态创建脚本。即使用DOM方法创建一个新的<script>元素,然后append到文档中,这个新创建的<script>元素加载了js文件,文件在该元素被添加到页面时开始下载,返回的代码通常会立刻执行(ff,opera会等待此前所有动态脚本节点执行完毕),这种方法的优势在于,文件的下载和执行过程不会阻塞页面其他进程。
    • 使用XMLHTTPRequest对象获取脚本注入页面。这种方法的优点是,你可以下载js代码但不会立刻执行,并且在所有主流浏览器中都兼容,而它的局限在于请求下载的脚本必须处于相同的域,这意味着无法从CDN下载,因此,大型的web应用通常不会采用这种方式。
    • 推荐使用的无阻塞模式。这种方法主要代表有LazyLoad,LABjs等一些延迟加载工具,当然自己也可以编写代码实现,原理并不复杂,主要就是在页面加载大量脚本的时候,先添加动态加载所需的代码,然后加载页面初始化所需的剩下的代码。

二、访问数据时,目标在作用域链中所处的位置越深,所花时间越长。

主要体现在两方面,其一是在使用闭包的时候,其二是在访问原型链的或者一些嵌套的对象的时候。

  1. 闭包在执行时,要访问的一些标识符并不在闭包自身作用域链中最顶端作用域,因此要访问大量跨作用域的标识符会导致性能的损失,此外,闭包的 scope 属性包含了一些对象引用,导致函数执行时创建的活动对象无法销毁,需要更多的内存开销。

  2. js中的继承方式有两种,一种是基于对象冒充,另一种是基于原型 prototype 的继承,后者会形成一条原型链,而我们在访问一个对象中的属性的时候,会先搜索对象本身,在搜索对象原型,在搜索父级的原型,一直往上,知道搜索到或者到原型链的顶端,这时就会导致性能问题,如果目标所处位置越深,找到它就越慢(一些经过特殊优化的浏览器除外),一个简单的例子,执行 location.href  比执行 window.location.href  要快,后者也比 window.location.href.toString() 要快,如果这些属性不是对象的实例属性,那么成员还要搜索原型链,这会花费更多的时间。

解决方法:

如果后面还需要用到之前访问的属性,则访问过后应该将它保存到局部变量中,局部变量在作用域链中通常都出于第一位。同时,避免with的使用,它会改变作用域链。

三、DOM的操作越多,代码运行越慢

简单的例子:

function count(){
  for(var i=0;i<50000;i++)
  document.getElementById(‘timt’).innerHTML=i;
}

修改后

function count(){
  for(var i=0;i<50000;i++)
}
document.getElementById(‘timt’).innerHTML=i;

显然修改后的肯定比之前的要快,前者每次循环都要查找一次DOM,当次数级数过大时,所花时间无疑是巨大的,解决的办法则是循环结束后一次写入。

DOM中的一些方法之间也有着不同的效率。实现同样的目的可以选择效率更高的方法。像在插入大量元素时使用innerHTML还是DOM创建节点的方法就要看情况了,在一些旧版本的浏览器中,使用innerHTML具有更大的优势,而在最新的Chrome中则是DOM方法更胜一筹。另外,使用DOM方法更新页面内容时,可以选择克隆已有元素而不是创建新的元素,在大多数浏览器中cloneNode都比document.createElement更有效率,尽管不是特别明显。遍历集合时应用局部变量缓存length属性,避免每次迭代更新以及重复获取花费更多的时间。

四、重绘与重排

Repaint(重绘)就是在一个元素的外观被改变,但没有改变布局(宽高)的情况下发生,如改变visibilityoutline、背景色等等。

 Reflow(重排)就是DOM的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树中受到影响的部分失效,浏览器会验证DOM树上的所有其它结点的visibility属性,这也是Reflow低效的原因。如:改变窗囗大小、改变文字大小、内容的改变、浏览器窗口变化,style属性的改变等等。如果Reflow的过于频繁,CPU使用率就会增加,因此要避免大量的 Repaint 和 Reflow

最小化重绘与重排的方法:

  1. 合并多次的DOM操作和样式修改

    例如,在修改多个样式属性时,尽可能的使用cssText属性或者更改className来实现,而不是多次的修改内联样式。

  2. 使元素脱离文档流后批量修改DOM,完成后再将元素带回文档中。

  有几种脱离文档的方式,以更新一个列表的内容为例:

  1. 改变display属性,临时从文档中移除ul元素,然后再恢复它。

    ul.style.display=”none”;
    appendDataToElement(ul, data);
    ul.style.display=”block”;
  2. 在文档之外创建文档片断,然后将它附加到原始列表中。

    var fragment=document.createDocumentFragment();
    appendDataToElement(fragment, data);
    document.getElementById(‘mylist’).appendChild(fragment);

    文档片断是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片断的一个便利语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。推荐使用这种方法。

  3. 为修改的节点创建一个备份,然后对副本操作,一旦操作完成,就用新的节点代替旧的节点。
    var old=document.getElementById(‘mylist’);
    var clone=old.cloneNode(true);
    appendDataToElement(clone,data);
    old.parentNode.replaceChild(clone,old);

另外,在DOM操作中,当要给页面中大量元素添加事件的时候,应尽可能的使用事件委托,减轻页面负担。在涉及到布局信息时,如我们需要获取一个元素的位置信息时,获取之后假如后面还要使用并且这个属性还没有变化时,应该将获取到的信息赋值给局部变量缓存起来,否则,浏览器会为了返回最新值刷新渲染队列,不利于优化。动画中使用绝对定位,减少重绘与重排。在IE中还存在一个与:hover相关的性能问题,避免对大量元素使用这种效果。

五、编程优化

  1. 算法。这没什么好说的,任何编程中,一个好的算法抵过很多优化措施,想学习算法知识的话可以看看算法导论,有兴趣做做ACM的题目也是不错的。
  2. 不要重复工作。也许最常见的重复工作就是浏览器探测。基于浏览器的功能分支判断导致大量的代码。以添加和移除事件监听为例,典型跨浏览器写法如下:
    funciton addHandler(target, eventType, handler){
      if(target.addEventListener){
        target.addEventListener(eventType, handler, false);
      }else {
        target.attachEvent(“on”+event,handler);
      }
    }
    funciton removeHandler(target, eventType, handler){
      if(target.removeEventListener){
          target.removeEventListener(eventType, handler, false);
      }else {
          target.detachEvent(“on”+event,handler);
      }
    }

    乍看之下似乎没有什么问题,而隐藏的性能问题在于每次调用函数都做了重复的判断,事实上,当检测过一次之后我们就已经知道这是什么浏览器了,而不必再重复检测了,只要在第一次检测之后,将检测方法重写成要运用的实际方法,那么下次重新调用这个方法检测时就不会再检测,而直接调用正确的方法执行了,因为我们第一次已经重写了这个方法。

    funciton addHandler(target, eventType, handler){
        addHandler=target.addEventListener ? function(target, eventType, handler){target.addEventListener(eventType, handler, false);} : function(arget, eventType, handler){target.attachEvent(“on”+event,handler);};
        addHandler(target, eventType, handler);
    }
    funciton removeHandler(target, eventType, handler){
      removeHandler=target.removeEventListener ? function(target, eventType, handler){target.removeEventListener(eventType, handler, false);}:function(target, eventType, handler){target.detachEvent(“on”+event,handler);}
      removeHandler(target, eventType, handler);
    }

     

以上也叫延迟加载。在第一次调用时,会先检查再调用,我们也可以使用预加载,即在代码执行时就判断采用哪种方法,并保存,就不必等到函数被调用时再检测了。

另外,尽量使用JS内置的方法,而不要去自己重新写一些原本js就有相似功能的函数,使用内置方法绝对要自己写的方法快。

六、其他

还有其他什么优化方法,我自己也不大熟悉了,像使用CDN加速什么的,虽然学过分布式,知道CDN的概念,但实际是怎么操作的就不知道了,所以在这就不细说了,另外还有像字符串和正则表达式什么的,关键还是自己的代码书写习惯,逻辑,正则表达式感觉挺难的,捕获组与非不获取,贪婪与非贪婪匹配。减少dns查找,避免重定向,添加expires头等运用好了也是提高性能的一大利器,关于AJAX数据传输则使用JSON格式的数据也是常用手段了。

还值得一说是,JS代码执行时间最好不要超过100ms,因为Javascript是单线程的,长时间的执行代码会使用户界面无法得到快速响应,对此,应该用定时器控制让出线程,等到界面更新后再继续执行剩下的代码。哦,最后别忘了,js写完了,上传之前要压缩!