《高性能javascript》阅读摘要

最近在阅读这本Nicholas C.Zakas(javascript高级程序设计做者)写的最佳实践、性能优化类的书。记录下主要知识。


加载和执行

脚本位置

放在<head>中的javascript文件会阻塞页面渲染:通常来讲浏览器中有多种线程:UI渲染线程、javascript引擎线程、浏览器事件触发线程、HTTP请求线程等。多线程之间会共享运行资源,浏览器的js会操做dom,影响渲染,因此js引擎线程和UI渲染线程是互斥的,致使执行js时会阻塞页面的渲染。
最佳实践:全部的script标签应尽量的放在body标签的底部,以尽可能减小对整个页面下载的影响。javascript

组织脚本

每一个<script>标签初始下载时都会阻塞页面渲染,因此应减小页面包含的<script>标签数量。内嵌脚本放在引用外链样式表的<link>标签以后会致使页面阻塞去等待样式表的下载,建议不要把内嵌脚本紧跟在<link>标签以后。外链javascript的HTTP请求还会带来额外的性能开销,减小脚本文件的数量将会改善性能。php

无阻塞的脚本

无阻塞脚本的意义在于在页面加载完成后才加载javascript代码。(window对象的load事件触发后)css

延迟的脚本

带有defer属性的<script>标签能够放置在文档的任何位置。对应的javascript文件将在页面解析到<script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发前)。当一个带有defer属性的javascript文件下载时,它不会阻塞浏览器的其余进程,能够与其余资源并行下载。执行的顺序是script、defer、load。html

动态脚本元素

使用javascript动态建立HTML中script元素,例如一些懒加载库。
优势:动态脚本加载凭借着它在跨浏览器兼容性和易用的有时,成为最通用的无阻塞加载解决方式。前端

XHR脚本注入

建立XHR对线个,用它下载javascript文件,经过动态建立script元素将代码注入页面中java

var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
  if(xht.readyState === 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      var script = document.createElement("script");
      script.type = "text/javascript";
      script.text = xhr.responseText;
      document.body.appendChild(script); 
    }
  }
};
xhr.send(null);

优势:能够下载javascript但不当即执行,在全部主流浏览器中均可以正常工做。
缺点:javascript文件必须与所请求的页面处于相同的域,意味着不能文件不能从CDN下载。
node


数据存取

存储的位置

数据存储的位置会很大程度上影响读取速度。c++

  • 字面量:字面量只表明自身,不存储在特定的位置。包括:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined。(我的理解:对象的指针自己是字面量)
  • 本地变量:var定义的数据存储单元。
  • 数组元素:存储在javascript数组内部,以数字为引。
  • 对象成员:存储在javascript对象内部,以字符串做为索引。

大多数状况下从一个字面量和一个局部变量中存取数据的差距是微不足道的。访问数据元素和对象成员的代价则高一点。若是在意运行速度,尽可能使用字面量和局部变量,减小数组和对象成员的使用。程序员

管理做用域

做用域链

每一个javascript函数都表示为一个对象,更确切的说是Function对象的一个实例。它也有仅供javascript引擎存储的内部属性,其中一个内部属性是[[Scope]],包含了一个被建立的做用域中对象的集合即做用域链。做用域链决定哪些数据能被函数访问。做用域中的每一个对象被称为一个可变对象。
当一个函数被建立后,做用域链会被建立函数的做用域中可访问的数据对象所填充。执行函数时会建立一个称为执行上下文的内部对象。执行上下文定义了函数执行时的环境。每次函数执行时对应的执行环境都是独一无二的,屡次调用同一个函数也会建立多个执行上下文,当函数执行完毕,执行上下文就会被销毁。每一个执行上下文都有本身的做用域链,用于解析标识符。当执行上下文被建立时,它的做用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些值按照它们出如今函数中的顺序,被复制到执行环境的做用域链中。这个过程一旦完成,一个被称为活动对象的新对象就为执行上下文建立好了。
活动对象做为函数运行时的变量对象,包含了全部局部对象,命名函数,参数集合以及this。而后此对象被推入做用域链的最前端。当执行环境被销毁时,活动对象也随之销毁。执行过程当中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索执行环境的做用域链,查找同名的标识符。搜索过程从做用域链头部开始,也就是当前运行函数的活动对象。若是找到,就使用这个标识符对应的变量,若是没找到,继续搜索做用域链的下一个对象知道找到,若没法搜索到匹配的对象,则标识符被看成未定义的。这个搜索过程影响了性能。web

标识符解析的性能

一个标识符所在的位置越深,读写速度就越慢,全局变量老是存在于执行环境做用域的最末端,所以它是最深的。
最佳实践:若是某个跨做用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。

改变做用域链

通常来讲一个执行上下文的做用域链是不会改变的。可是,with语句和try-catch语句的catch子语句能够改变做用域链。
with语句用来给对象的全部属性建立一个变量,能够避免屡次书写。可是存在性能问题:代码执行到with语句时,执行环境的做用域链临时被改变了,建立了一个新的(包含了with对象全部属性)对象被建立了,以前全部的局部变量如今处于第二个做用域链对象中,提升了访问的代价。建议放弃使用with语句。
try-catch语句中的catch子句也能够改变做用域链,当try代码块中发生错误,执行过程会自动跳转到catch子句,把异常对象推入一个变量对象并置于做用域的首位,局部变量处于第二个做用域链对象中。简化代码可使catch子句对性能的影响下降。
最佳实践:将错误委托给一个函数来处理。

动态做用域

不管with语句仍是try-catch语句的子句catch子句、eval()语句,都被认为是动态做用域。通过优化的javascript引擎,尝试经过分析代码来肯定哪些变量是能够在特定的时候被访问,避开了传统的做用域链,取代以标识符索引的方式快速查找。当涉及动态做用域时,这种优化方式就失效了。
最佳实践:只在确实有必要时使用动态做用域。

闭包、做用域和内存

因为闭包的[[Scope]]属性包含了与执行上下文做用域链相同的对象的引用,所以会产生反作用。一般来讲,函数的活动对象会随着执行环境一同销毁。但引入闭包时,因为引用仍然存在闭包的[[Scope]]属性中,所以激活对象没法被销毁,致使更多的内存开销。

最须要关注的性能点:闭包频繁访问跨做用域的标识符,每次访问都会带来性能损失。

最佳实践:将经常使用的跨做用域变量存储在局部变量中,而后直接访问局部变量。

对象成员

不管是经过建立自定义对象仍是使用内置对象都会致使频繁的访问对象成员。

原型

javascript中的对象是基于原型的。解析对象成员的过程与解析变量十分类似,会从对象的实例开始,若是实例中没有,会一直沿着原型链向上搜索,直到找到或者到原型链的尽头。对象在原型链中位置越深,找到它也就越慢。搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上遍历原型链带来的开销,这让性能问题更为严重。

嵌套成员

对象成员可能包含其余成员,每次遇到点操做符"."会致使javascript引擎搜索全部对象成员。

缓存对象成员值

因为全部相似的性能问题都与对象成员有关,所以应该尽量避免使用他们,只在必要时使用对象成员,例如,在同一个函数中没有必要屡次读取同一个对象属性(保存到局部变量中),除非它的值变了。这种方法不推荐用于对象的方法,由于将对象方法保存在局部变量中会致使this绑定到window,致使javascript引擎没法正确的解析它的对象成员,进而致使程序出错。


DOM编程

浏览器中的DOM

文档对象模型(DOM)是一个独立于语言的,用于操做XML和HTML文档的程序接口API。DOM是个与语言无关的API,在浏览器中的接口是用javascript实现的。客户端脚本编程大多数时候是在和底层文档打交道,DOM就成为如今javascript编码中的重要组成部分。浏览器把DOM和javascript单独实现,使用不一样的引擎。

天生就慢

DOM和javascript就像两个岛屿经过收费桥梁链接,每次经过都要缴纳“过桥费”。
推荐的作法是尽量减小过桥的次数,努力待在ECMAScript岛上。

DOM访问与修改

访问DOM元素是有代价的——前面的提到的“过桥费”。修改元素则更为昂贵,由于它会致使浏览器从新计算页面的几何变化(重排)。最坏的状况是在循环中访问或修改元素,尤为是对HTML元素集合循环操做。
在循环访问页面元素的内容时,最佳实践是用局部变量存储修改中的内容,在循环结束后一次性写入。
通用的经验法则是:减小访问DOM的次数,把运算尽可能留在ECMAScript中处理。

节点克隆

大多数浏览器中使用节点克隆都比建立新元素要更有效率。

选择API

使用css选择器也是一种定位节点的便利途径,浏览器提供了一个名为querySelectorAll()的原生DOM方法。这种方法比使用javascript和DOM来遍历查找元素快不少。使用另外一个便利方法——querySelector()来获取第一个匹配的节点。

重绘与重排

浏览器下载完页面中的全部组件——HTML标记、javascript、CSS、图片——以后会解析并生成两个内部的数据结构:DOM树(表示页面结构)、渲染树(表示DOM节点如何显示)。当DOM的变化影响了元素的几何属性,浏览器会使渲染树中受到影响的部分失效,并重构,这个过程成为重排,完成后,会从新绘制受影响的部分到屏幕,该过程叫重绘。并非全部的DOM变化都会影响几何属性,这时只发生重绘。重绘和重排会致使web应用程序的UI反应迟钝,应该尽可能避免。

重排什么时候发生

当页面布局的几何属性改变时就须要重排:

1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(包括:外边据、内边距、边框厚度、宽度、高度等属性改变)
  4. 内容改变,例如:文本改变或图片被另外一个不一样尺寸的图片代替
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变
渲染树变化的排队与刷新

因为每次重排都会产生计算消耗,大多数浏览器经过队列化修改并批量执行来优化重排过程。可是有些操做会致使强制刷新队列并要求任务马上执行:

1. offsetTop,offsetLeft,offsetWidth,offsetHeight
  2. scrollTop,scrollLeft,scrollWidth,scrollHeight
  3. clientTop,clientLeft,clientWidth,clientHeight
  4. getComputedStyle()

以上属性和方法须要返回最新的布局信息,所以浏览器不得不执行渲染队列中的修改变化并触发重排以返回正确的值。
最佳实践:尽可能将修改语句放在一块儿,查询语句放在一块儿。

最小化重绘和重排

为了减小发生次数,应该合并屡次DOM的样式的修改,而后一次处理掉。

批量修改DOM

当你须要对DOM元素进行一系列操做时,能够经过如下步骤来减小重绘和重排的次数:

1. 使元素脱离文档
  2. 对其应用多重改变
  3. 把元素带回文档流

该过程会触发两次重排——第一步和第三步,若是忽略这两步,在第二步所产生的任何修改都会触发一次重排。

有三种基本的方法可使DOM脱离文档:

  1. 隐藏元素,应用修改,从新显示
  2. 使用文档片断,在当前DOM以外构建一个子树,再把它拷贝回文档
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

推荐使用文档片断,由于它们所产生的DOM遍历和重排次数最少。

缓存缓存布局信息

当你查询布局信息时,浏览器为了返回最新值,会刷新队列并应用全部变动。
最佳实践:尽可能减小布局信息的获取次数,获取后把它赋值给局部变量,而后操做局部变量。

让元素脱离动画流

用展开、折叠的方式来显示和隐藏部分页面是一种常见的交互模式。一般包括展开区域的几何动画,并将页面其余部分推向下方。通常来讲,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所须要重排的次数越少,应用程序的响应速度就越快。当一个动画改变整个页面的余下部分时,会致使大规模重排。节点越多状况越差。避免大规模的重排:

1. 使用绝对定位页面上的动画元素,将其脱离文档流。
  2. 应用动画
  3. 当动画结束时回恢复定位,从而只会下移一次文档的其余元素。

这样只形成了页面的一个小区域的重绘,不会产生重排并重绘页面的大部份内容。

:hover

若是有大量元素使用了:hover,那么会下降响应速度。此问题在IE8中更为明显。

事件委托

当页面中存在大量元素,而且每个都要一次或屡次绑定事件处理器时,这种状况可能会影响性能,每绑定一个事件处理器都是有代价的,它要么加剧了页面负担(更多的代码、标签),要么增长了运行期的执行时间。须要访问和修改的DOM元素越多,应用程序就越慢,特别是事件绑定一般发生在onload时,此时对每个富交互应用的网页来讲都是一个拥堵的时刻。事件绑定占用了处理事件,并且浏览器要跟踪每一个事件处理器,这也会占用更多的内存。这些事件处理器中的绝大部分均可能不会被触发。
事件委托原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只须要给外层元素绑定一个处理器,就能够处理在其子元素上触发的全部事件。
根据DOM标准,每一个事件都要经历三个阶段:

1. 捕获
  2. 到达目标
  3. 冒泡

IE不支持捕获,可是对于委托而言,冒泡已经足够。

<body>
     <div>     
          <ul id="menu">
               <li>
                    <a href="menu1.html">menu #1</a>
               </li>
               <li>
                    <a href="menu1.html">menu #2</a>
               </li>
          </ul>
     </div>
</body>

在以上的代码中,当用户点击连接“menu #1”,点击事件首先从a标签元素收到,而后向DOM树上层冒泡,被li标签接收而后是ul标签而后是div标签,一直到达document的顶层甚至window。
委托实例:阻止默认行为(打开连接),只须要给全部连接的外层UL"menu"元素添加一个点击监听器,它会捕获并分析点击是否来自连接。

document.getElementById('menu').onclick = function(e) {
          //浏览器target
          e=e||window.event;
          var target = e.target||e.srcElement;

          var pageid,hrefparts;
          
          //只关心hrefs,非连接点击则退出,注意此处是大写
          if (target.nodeName !== 'A') {
         return;
          }

          //从连接中找出页面ID
          hrefparts = target.href.split('/');
          pageid = hrefparts[hrefparts.length-1];
          pageid = pageid.replace('.html','');

          //更新页面
          ajaxRequest('xhr.php?page='+id,updatePageContents);

          //浏览器阻止默认行为并取消冒泡
          if (type of e.preventDefault === 'function') {
               e.preventDefault();
               e.stopPropagation();
          } else {
               e.returnValue=false;
               e.cancelBubble=true;
          }
};

跨浏览器兼容部分:

1. 访问事件对象,并判断事件源
  2. 取消文档树中的冒泡(可选)
  3. 阻止默认动做(可选)

算法和流程控制

循环

循环的类型

ECMA-262标准第三版定义了javascript的基本语法和行为,其中共有四种循环。

1. 第一种是标准的for循环。它由四部分组成:初始化、前测条件、后执行体、循环体。
           for (var i=0;i<10;i++){
                //do something
           }

for循环是javascript最经常使用的循环结构,直观的代码封装风格被开发者喜好。

2. while循环。while循环是最简单的前测循环,由一个前测条件和一个循环体构成。

  3. do-while循环是javascript惟一一种后测循环,由一个循环体和一个后测条件组成,至少会执行一次。
  4. for-in循环。能够枚举任何对象的属性名。
循环的性能

javascript提供的四种循环类型中,只有for-in循环比其余几种明显要慢。由于每次迭代操做会同时搜索实例或原型属性,for-in循环的每次迭代都会产生更多开销。速度只有其余类型循环的七分之一。除非你明确须要迭代一个属性数量未知的对象,不然应该避免使用for-in循环。若是你须要遍历一个数量有限的已知属性列表,使用其余循环类型会更快,好比数组。
除for-in外,其余循环类型的性能都差很少,类型的选择应该基于需求而不是性能。

提升循环的性能
1. 减小每次迭代处理的事务
  2. 减小迭代的次数
减小迭代的工做量

减小对象成员及数组项的查找次数。

在不影响的结果的状况下,可使用倒序来略微提高性能。由于控制条件只要简单的与零比较。控制条件与true比较时,任何非零数会自动转换为true,而零值等同于false,实际上从两次比较(迭代数少于总数么?是否为true?)减小到一次比较(它是true么)。当循环复杂度为O(n)时,减小每次迭代的工做量是最有效的方法。当复杂度大于O(n)时,建议着重减小迭代次数。
减小迭代次数

Duff's Device是一个循环体展开技术,使得一次迭代中实际上执行了屡次迭代的操做。一个典型的实现以下:

//credit:Jeff Greenberg
var iterations = Math.floor(items.length / 8),
      startAt = items.length/8,
      i = 0;
do{
      switch(startAt){
                case 0: process(items[i++]);
                case 7: process(items[i++]);
                case 6: process(items[i++]);
                case 5: process(items[i++]);
                case 4: process(items[i++]);
                case 3: process(items[i++]);
                case 2: process(items[i++]);
                case 1: process(items[i++]);
      }
      startAt = 0;
} while (--iterations);

Duff's Device背后的基本理念是:每次循环中最多能够调用8此process()。循环的迭代次数除以8。因为不是全部数字都能被8整除,变量startAt用来存放余数,表示第一次循环中应该调用多少次process()。
此算法稍快的版本取消了switch语句,并将余数处理和主循环分开

//credit:Jeff Greenberg
var i = items.length % 8;
while(i){
        process(item[i--]);
}
i = Math.floor(items.length / 8);
while(i){
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
}

尽管这种实现方法用两次循环代替以前的一次循环,但它移除了循环体中的switch语句,速度比原始循环更快。
若是循环迭代的次数小于1000,可能它与常规循环结构相比只有微不足道的性能提高。若是迭代数超过1000,那么执行效率将明显提高。例如在500000此迭代中,其运行时间比常规循环减小70%

基于函数的迭代

ECMA-262第四版加入的数组方法:forEach()方法。此方法遍历一个数组的全部成员,并在每一个成员上执行一个函数。要运行的函数做为参数传给forEach(),并在调用时接受三个参数,分别是当前的值、索引以及数组自己。尽管基于函数的迭代提供了一个更为便利的迭代方法,但它仍比基于循环的迭代要慢一些。对每一个数组项调用外部方法所带来的开销是速度慢的主要缘由。

条件语句

if-else对比switch

条件数数量越大,越倾向于使用switch,主要是由于易读性。事实证实,大多数状况下switch比if-else运行得要快,但只有条件数量很大时才快得明显。

优化if-else

最小化到达正确分支前所须要判断的条件数量。最简单的优化方法是确保最可能出线的条件放在首位。if-else中的条件语句应该老是按照从最大几率到最小几率的顺序排列,以确保运行速度最快。假设均匀分部,可以使用二分法的思想,重写为一系列嵌套的if-else语句。

查找表

有些时候优化条件语句的最佳方案是避免使用if-else和switch。可使用数组和普通对象来构建查找表,经过查找表访问数据比用if-else或switch快不少。当单个键值存在逻辑映射时,构建查找表的优点就能体现出来。(好比把按照顺序的键值映射放到数组里)

递归

使用递归能够把复杂的算法变的简单。潜在问题是终止条件不明确或缺乏终止条件会致使函数长时间运行,并使得用户界面处于假死状态和浏览器的调用栈大小限制。

调用栈限制

javascript引擎支持的递归数量与javascript调用栈大小直接相关。

递归模式

当你遇到调用栈大小限制时,第一步应该检查代码中的递归实例。有两种递归模式,第一种是调用自身,很容易定位错误。第二种是互相调用,很难定位。

迭代

任何递归能实现的算法一样可使用迭代来实现。使用优化后的循环代替长时间运行的递归函数能够提高性能,由于运行一个循环比反复调用一个函数的开销要少的多。
归并排序算法是最多见的用递归实现的算法:

function merge(left, right) {
    var result = [];

    while (left.length > 0 && right.length > 0){
        if (left[0] < right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());    
        }
    }

    return result.concat(left).concat(right);
}

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
        return merge(mergeSort(left),mergeSort(right));
}

使用迭代实现归并算法:

//使用和上面相同的merge函数

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var work = [];
    for (var i=0, len=items.length;i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);

    for (var lim=len; lim>1; lim = (lim+1)/2){
        for (var j=0,k=0; k < lim; j++, k+=2){
            work[j] = merge(work[k],work[k+1]);
        }
        work[j] = [];
    }

    return work[0];
}

尽管迭代版本的归并排序算法比递归实现得要慢一些,但它不会像递归版本那样受到调用栈限制的影响。把递归算法改用迭代实现是避免栈溢出错误的方法之一

Memoization

Memoization是一种避免重复工做的方法,它缓存前一个计算结果供后续计算使用,避免了重复工做。
使用Memoization技术来重写阶乘函数:

function memfactorial(n){
    if(!memfactorial.cache){
        memfactorial.cache={
            "0":1,
            "1":1
        };
    }

    if(!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial (n-1);
    }

    return memfactorial.cache[n];
}

字符串和正则表达式

字符串连接

+和+=

不该在等号右边进行和被赋值的量无关的字符串拼接运算,这样会创造临时字符串。
例如:

str += "one" + "two";

会经历四个步骤:

1. 在内存中建立一个临时字符串
  2. 链接后的字符串“onetwo”被赋值给该临时字符串
  3. 临时字符串与str当前的值链接
  4. 结果赋值给str

使用这种方式来代替:

str = str + "one" + "two";
//等价于 str = ((str + "one") + "two")

赋值表达式由str开始做为基础,每次给它附加一个字符串,由作到右一次链接,所以避免了使用临时字符串。

数组项合并

Array.prototype.join方法将数组的全部元素合并成一个字符串,它接受一个字符串参数做为分隔符插入每一个元素的中间。大多数浏览器中,数组项合并比其余字符串链接的方法更慢。

String.prototype.concat

字符串的原生方法concat能接收任意数量的参数,并将每个参数附加到所调用的字符串上。这是最灵活的字符串合并方法。多数状况下,使用concat比使用简单的+和+=稍慢。

正则表达式优化

部分匹配比彻底不匹配所用的时间要长。

正则表达式工做原理
1.  第一步编译

浏览器会验证正则表达式,而后把它转换为一个原生代码程序,用于执行匹配工 做。若是把正则对象赋值给一个变量,能够避免重复这一步。

2.  第二步设置起始位置
  3.  第三步匹配每一个正则表达式字元
  4.  第四步匹配成功或失败
回溯

当正则比到达时匹配目标字符串时,从左到右逐个测试表达式的组成部分,看是否能找到匹配项。在遇到量词和分支时,须要决策下一步如何处理。若是遇到量词,正则表达式需决定什么时候尝试匹配更多字符;若是遇到分支,那么必须从可选项中选择一个尝试匹配。每当正则表达式作相似的决定时,若是有必要的话,都会记录其余选择,以备返回时使用。若是当前选项匹配成功,正则表达式继续扫描表达式,若是其余部分也匹配成功,尼玛匹配结束。可是若是当前选项找不到匹配值,或后面的部分匹配失败,那么正则表达式会回溯到最后一个决策点,而后在剩余的选项中选择一个。这个过程会一直进行,知道找到匹配项,或者正则表达式中量词和分支选项的全部排列组合都尝试失败,那么它将放弃匹配从而移动到字符串的下一个字符,再重复此过程。

重复和回溯

贪婪匹配是段尾一个个回溯接下来的匹配内容,惰性正好相反;

回调失控

最佳实践:若是你的正则表达式包含了多个捕获组,那么你须要使用适当的反向引用次数。

嵌套量词与回溯失控

所谓的嵌套量词须要格外的关注且当心使用,以确保不会引起潜在的回溯失控。嵌套两次是指两次出线在一个自身被重复量词修饰的组中。确保正则表达式的两个部分不能对字符串的相同部分进行匹配

更多提升正则表达式效率的方法
1.  关于如何让正则匹配更快失败

正则表达式慢的缘由一般是匹配失败的过程慢。

2.  正则表达式以简单、必需的字元开始

一个正则表达式的起始标记应当尽量快速的测试并排除明显不匹配的位置。尽可能以一个锚、特定字符串、字符类和单词边界开始,尽可能避免以分组或选择字元开头,避免顶层分支。

3.  使用量词模式,使它们后面的字元互斥

当字符与字元相邻或子表达式可以重叠匹配时,正则表达式尝试拆解文本的路径数量将增长。

4.  减小分支数量,缩小分支范围

分支使用竖线|可能要求在字符串的每个位置上测试全部的分支选项。你一般能够经过使用字符集和选项组件来减小对分支的需求,或将分支在正则表达式上的位置推后。

5.  使用非捕获组

捕获组消耗时间和内存来记录反向引用,并使它保持最新。若是你不须要一个反向引用,可使用非捕获组来避免这些开销。

6.  只捕获感兴趣的文本以减小后处理

若是须要引用匹配的一部分,应该才去一切手段捕获那些片断,再使用反向引用来处理。

7.  暴露必需的字元

尝试让正则表达式引擎更容易判断哪些字元是必需的。

8.  使用合适的量词
9.  把正则表达式赋值给变量并重用它们

避免在循环体中重复编译正则表达式。

10.  将复杂的正则表达式拆分为简单的片断

什么时候不使用正则表达式

当只是搜索字面字符串,尤为是事先知道字符串的哪一部分将要被查找时。正则表达式没法直接跳到字符串末尾而不考虑沿途的字符。


快速响应的用户界面

浏览器UI线程

用于执行Javascript和更新用户界面的进程一般被称为“浏览器UI线程”。UI线程的工做基于一个见到那的队列系统,任务会被保存到队列中直到线程空闲。

浏览器限制

浏览器限制了javascript的运行时间。此类限制分为两种:调用栈的大小限制和长时间运行脚本限制。

多久算过久

单个Javascript操做话费的总时间不该该超过100毫秒。
最佳实践:限制全部的Javascript任务在100毫秒或更短的时间内完成。

使用定时器让出时间片断

当Javascript不能在100毫秒或更短的时间内完成。最理想的方法是让出UI线程的控制权,使得UI能够更新。

定时器基础

在Javascript中可使用setTimeout()和setInterval()建立定时器,它们接收相同的参数:要执行的函数和执行前的等待时间。定时器与UI线程的交互:定时器会告诉Javascript引擎先等待必定时间,而后添加一个Javascript任务到UI队列。定时器代码只有在建立它的函数执行完以后,才有可能执行。不管发生何种状况,建立一个定时器会形成UI线程暂停,如同它从一个任务切换到下一个任务。所以,定时器代码会重置全部相关的浏览器限制,包括 长时间运行脚本定时器。此外,调用栈也会在定时器中重置为0。setTimeout()和setInterval()几近相同,若是 UI队列中已经存在由同一个setInterval()建立的任务,那么后续任务不会被添加到UI队列中。若是setTimeout()中的函数须要消耗比定时器延时更长的运行时间,那么定时器代码中的延时几乎是不可见的。

定时器的精度

Javascript定时器延迟一般不太准确,相差大约为几毫秒,没法用来精确计算时间。并且还存在最小值的限制。

使用定时器处理数组

是否能够用定时器取代循环的两个决定性因素:处理过程是否必须同步;数据是否必须按照顺序处理;若是两个答案都是否,那么代码适用于定时器分解任务。

var todo = items.concat();
// 克隆原数组

setTimeout(function(){

    // 取得数组的下一个元素并进行处理
    process(todo.shift());

    // 若是还有须要处理的元素,建立另外一个定时器
    if(todo.length > 0){
        setTimeout(arguments.callee, 25);
    } else {
        callback(items);
    }

}, 25);

每一个定时器的真实延时在很程度上取决于具体状况。广泛来说,最好使用至少25毫秒,由于再小的延时,对大多数UI更新来讲不够用。

记录代码运行使劲啊

经过定时器建立Date对象并比较它们的值来记录代码运行事件。加号能够将Date对象转换成数字,那么在后续的运算中就无须转换了。避免把任务分解成过于零碎的碎片,由于定时器之间有最小间隔,会致使出线空闲事件。

定时器与性能

当多个重复的定时器同时建立每每会出线性能问题。由于只有一个UI线程,而全部的定时器都在争夺运行时间。那些间隔在1秒或1秒以上的低频率的重复定时器几乎不会影响Web应用的响应速度。这种状况下定时器延迟远远超过UI线程产生瓶颈的值,能够安全的重复使用。当过个定时器使用较高的频率(100到200毫秒之间)时,会明显影响性能。在web应用中限制高频率重复定时器的数量,做为代替方案,使用一个独立的重复定时器每次执行多个操做。

Web Worker

引入了一个接口,能使代码运行而且不占用浏览器UI线程的时间。

Worker

没有绑定UI线程,每一个Web Worker都有本身的全局环境,其功能只是Javascript特性的一个子集。运行环境由以下部分组成:一个navigator对象,值包括四个属性:appName、appVersion、userAgent和platform。
一个location对象(与window.location相同,不过全部属性都是只读的。)。
一个self对象,指向全局worker对象。
一个importScipt()方法,用来加载Worker所用到的外部javascript文件。
全部的ECMAScript对象
XMLHttpRequest构造器
setTimeout()方法和setInterval()方法
一个close()方法,它能马上中止Worker运行
因为Web Worker有着不一样的全局运行环境,所以你没法从javascript代码中建立它。须要建立一个彻底独立的javascript文件,其中包含了须要在Worker中运行的代码。要建立网页人工线程,你必须传入这个javascript文件的URL;

与Worker通讯

经过事件接口进行通讯。网页代码能够经过postMessage()方法给Worker传递数据,它接受一个参数,即须要传递给Worker的数据。此外,Worker还有一个用来接收信息的onmessage事件处理器。Worker可经过它本身的postMessage()方法把信息回传给页面。消息系统是网页和Worker通讯的惟一途径。只有特定类型的数据可使用postMessage()传递。你能够传递原始值(字符串、数字、布尔值、null和undefined),也能够传递Object和Array的实例,其余类型就不容许了。有效数据会被序列化,传入或传出Worker,而后反序列化。虽然看上去对象能够直接传入,但对象实例彻底是相同数据的独立表述。

加载外部文件

Worker 经过importScript()方法加载外部javascript文件,该方法接收一个或多个javascript文件URL做为参数。importScript()的调用过程是阻塞式的,知道全部全部文件加载并执行完成以后,脚本才会继续运行。因为Worker在UI线程以外运行,因此这种阻塞并不会影响UI响应。
Web Worker适合用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本。尽管它看上去用处不大,但Web应用中一般有一些数据处理功能将收益于Worker而不是定时器。
可能的用处:

  1. 编码/解码大字符串
  2. 复杂数学运算
  3. 大数组排序
  4. 任何超过100毫秒的处理过程,都应当考虑Worker方案是否是比基于定时器的方案更为合适。

Ajax

Ajax是高性能javascript的基础。它能够经过延迟下载体积较大的资源文件来使得页面加载速度更快。它经过异步的方式在客户端和服务端之间传输数据,避免同时传输大量数据。

数据传输

请求数据

有五种经常使用技术用于想服务器请求数据:

  1. XMLHttpRequest
  2. Dynamic script tag insertion(脚本动态注入)
  3. iframes
  4. Comet
  5. Multipart XHR

现代高性能Javascript中使用的三种技术是:XHR、动态脚本注入和Multipart XHR

XMLHttpRequest

XMLHttpRequest是目前最经常使用的技术,它容许异步发送和接收数据。因为XHR提供了高级的控制,因此浏览器对其增长了一些限制。你不能使用XHR从外域请求数据。对于那些不会改变服务器状态,只会获取数据(幂等行为)的请求,应该使用GET。经GET请求的数据会被缓存起来,若是须要屡次请求统一数据的话,它会有助于提高性能。只有当请求的URL加上参数的长度接近或超过2048个字符时,才应该用POST获取数据。由于IE限制URL长度,过长将致使请求的URL被截断。

动态脚本注入

这种技术客服了XHR的最大限制:它能跨域请求数据。这是一个Hack,你不须要实例化一个专用对象,而可使用javascript建立一个新的脚本标签,并设置它的src属性为不一样域的URL。与XHR相比,动态脚本注入提供的控制是有限的。只能使用GET方法而不是POST方法。不能设置请求的超时处理或重试;不能访问请求的头部信息,不能把整个响应信息做为字符串来处理。由于响应消息做为脚本标签的源码,它必须是可执行的javascript代码。你不能使用纯XML、纯JSOn或其余任何格式的数据,不管哪一种格式,都必须封装在一个回调函数中。这项技术的速度却很是快。响应消息是做为javascript执行,而不是做为字符串须要进一步处理。正因如此,它有潜力成为客户端获取并解析数据最快的方法。

Multipart XHR

容许客户端只用一个HTTP请求就能够从服务端向客户端传送多个字元。它经过在服务端将字元打包成一个由双方约定的字符串分割的长字符串并发送到客户端。而后用javascript代码处理这个长字符串,并根据它的mime-type类型和传入的其余“头信息”解析出每一个资源。缺点:资源不能被浏览器缓存。
能显著提升性能的场景:
页面包含了大量其余地方用不到的资源,尤为是图片;
网站已经在每一个页面中使用了一个独立打包的Javascript或CSS文件以减小http请求;

发送数据

XMLHttpRequest
当使用XHR发送数据到服务器时,GET方式会更快。这是由于,对少许数据而言一个GET请求只发送一个数据包。而一个POST请求至少要发两个数据包,一个装载头信息,另外一个装载POST正文。POST更适合发送大量数据到服务器,由于它不关心额外数据包的数量,另外一个缘由是URL长度有限制,它不可能使用过长的GET请求。

Beacons

相似动态脚本注入。使用Javascript建立一个新的Image对象,并把src属性设置为服务器上脚本的URL。该URL包含了咱们要经过GET传回的键值对数据。服务器会接受数据并保存下来,无须向客服端发送任何回馈信息,所以没有图片会实际显示出来。这是回传信息最有效的方式。性能消耗更小,并且服务器端的错误不影响客户端。缺点:没法发送POST数据,而URL的长度有最大值,因此能够发送的数据的长度被限制的至关小。

数据格式

考虑数据格式时惟一须要比较的标准就是速度

XML

当Ajax最早开始流行时,它选择了XML做为数据格式。优点:极佳的通用性、格式严格,且易于验证。缺点:冗长,依赖大量结构、有效数据的比例很低、语法模糊,若是有其余格式可选不要使用它。

JSON

是一种使用Javascript对象和数组直接量编写的轻量级且易于解析的数据格式。

JSON-P

事实上,JSON能够被本地执行会致使几个重要的性能影响。当使用XHR时,JSON数据被当成字符串返回。在使用动态脚本注入时,JSON数据要被当成另外一个Javascript文件并做为原生代码执行,为实现这一点必须封装在一个回调函数中。JSON-P由于回调包装的缘由略微增大了文件尺寸,但性能提高巨大。因为数据是看成原生的Javascript,所以解析速度跟原生Javascript同样快。最快的JSON格式是使用数组形式的JSON-P。不要把敏感数据编码在JSON-P中,由于没法确认它是否保持着私有调用状态。

HTML

一般你请求的数据须要被转换成HTML以显示到页面上。Javascript能够较快地把一个较大的数据结构转换成简单的HTML,但在服务器处理会快不少。一种可考虑的技术是在服务器上构建好整个HTML再传回客户端,Javascript能够很方便地经过innerHTML属性把它插入页面相应的位置。取点:臃肿的数据格式、比XML更繁杂。在数据自己的最外层,能够嵌套HTML标签,每一个都带有id、class和其余属性。HTML格式可能比实际数据占用更多空间。应当在客户端的瓶颈是CPU而不是带宽时才使用此技术。

自定义格式

理想的数据格式应该只包含必要的结构,以便你能够分解出每一个独立的字段。最重要的决定就是采用哪一种分隔符,它应当是一个单字符,并且不该该存在你的数据中。

Ajax性能指南

缓存数据

在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存。
在客户端,把获取到的信息存储到本地,从而避免再次请求。

设置HTTP头信息

若是但愿ajax能被浏览器缓存,那么你必须使用GET方式发送请求而且须要在响应中发送正确的HTTP头信息。Expires头信息会告诉浏览器应该缓存多久。它的值是一个日期。

本地数据存储

直接把从服务器接收到的数据储存起来。能够把响应文本保存到一个对象中,以URL为键值做为索引。

Ajax类库的局限性

全部的Javascript类库都容许你访问一个Ajax对象,它屏蔽了浏览器之间的差别,给你一个统一的接口。为了统一接口的功能,类库简化接口,使得你不能访问XMLHttpRequest的完整功能。


编程实践

避免双重求值

Javascript容许你在程序中提取一个包含代码的字符串,而后动态执行它。有四种标准方法能够实现:eval()、Function()构造函数、setTimeout()和setInterval()。首先会以正常的方式求值,而后在执行的过程当中对包含于字符串的代码发起另外一个求值运算。每次使用这些方法都要建立一个新的解释器/编译器实例,致使消耗时间大大增长。
大多数时候没有必要使用eval()和Function(),所以最好避免使用它们。定时器则建议传入函数而不是字符串做为第一个参数。

使用Object/Array直接量

Javascript中建立对象和数组的方法有多种,但使用对象和数组直接量是最快的方式。

避免重复工做

别作可有可无的工做,别重复作已经完成的工做。

延迟加载

第一次被调用时,会先检查并决定使用哪一种方法去绑定或取消绑定事件处理器。而后原始函数被包含正确操做的新函数覆盖。最后一步调用新的函数,并传入原始参数。随后每次调用都不会再作检测,由于检测代码已经被新的函数覆盖。调用延迟加载函数时,第一次总会消耗较长的费时间,由于它必须运行检测接着再调用另外一个函数完成任务。但随后调用相同的函数会更快,由于不须要再执行检测逻辑。当一个函数在页面中不会马上调用时,延迟加载是最好的选择。

条件预加载

它会在脚本加载期间提早检测,而不会等到函数被调用。检测的操做依然只有一次,知识它在过程当中来的更早。条件预加载确保全部函数调用消耗的时间相同。其代价是须要在脚本加载时就检测,而不是加载后。预加载适用于一个函数立刻就要被用到,而且在整个页面的生命周期中频繁出现的场合。

使用快的部分

运行速度慢的部分其实是代码,引擎一般是处理过程当中最快的部分。

位操做

使用位运算代替纯数学操做:对2的取模运算能够被&1代替,速度提升不少。位掩码:处理同时存在多个布尔选项时的情形,思路是使用单个数字的每一位来断定是否选项成立,从而有效得把数字转换为布尔值标记组成的数组。

原生方法

原生方法更快,由于写代码前就存在浏览器中了,而且都是用底层语言好比c++编写的。经验不足的Javascript开发者常常犯的错误就是在代码中进行复杂的数学运算,而没有使用内置的Math对象中那些性能更好的版本。另外一个例子是选择器API,它容许使用CSS选择器来查找DOM节点。原生的querySelector()和querySelectorAll()方法完成任务平均所需时间是基于Javascript的CSS查询的10%。


构建并部署高性能Javascript应用

合并多个Javascript文件,网站提速指南中第一条也是最重要的一条规则,就是减小http请求数。

预处理Javascript文件

预处理你的Javascript源文件并不会让应用变的更快,但它容许你作些其余的事情,例若有条件地插入测试代码,来衡量你的应用程序的性能。

Javascript压缩

指的是把Javascript文件中全部与运行无关的部分进行剥离的过程。剥离的内容包括注释和没必要要的空白字符。该过程一般能够将文件大小减半,促使文件更快被下载,并鼓励程序员编写更好的更详细的注释。

构建时处理对比运行时处理

广泛规则是只要能在构建时完成的工做,就不要留到运行时去作。

Javascript的http压缩

当Web浏览器请求一个资源时,它一般会发送一个Accept-Encoding HTTP头来告诉Web服务器它支持哪一种编码转换类型。这个信息主要用来压缩文档以更快的下载,从而改善用户体验。Accept-Encoding可用的值包括:gzip、compress、deflate和identity。gzip是目前最流行的编码方式。它一般能减小70%的下载量,成为提高Web应用性能的首选武器。记住Gzip压缩主要适用于文本,包括Javascript文件。

缓存Javascript文件

缓存HTTP组件能极大提升网站回访用户的体验。Web服务器经过Expires HTTP响应头来告诉客户端一个字元应当缓存多长事件。它的值是一个遵循RFC1123标准的绝对时间戳。

处理缓存问题

适当的缓存控制能提高用户体验,但它有一个缺点:当应用升级时,你须要确保用户下载到最新的静态内容。这个问题能够经过把改动过的静态资源重命名来解决。

使用内容分发网络(CDN)

内容分发网络是在互联网上按地理位置设置分部计算机网络,它负责传递内容给终端用户。使用CDN的主要缘由是加强Web应用的可靠性、可扩展性,更重要的是提高性能。事实上,经过向地理位置最近的用户输入内容,CDN能极大减小网络延时。

相关文章
相关标签/搜索