20200430-20200628《高性能JavaScript》读书笔记

《高性能JavaScript》

本书小结汇总

  • 第1章 加载和执行
    • </body>闭合标签之前,将所有的<script>标签放到页面底部。这能确保在脚本执行前页面已经完成了渲染。
    • 合并脚本。页面中的<script>标签越少,加载也就越快,响应也更迅速。无论外链文件还是内嵌脚本都是如此。
    • 有多种无阻塞下载js的方法:
      • 使用<script>标签的defer属性;
      • 使用动态创建的<script>元素来下载并执行代码;
      • 使用XHR对象下载js代码并注入页面中。
  • 第2章 数据存取
    • 访问字面量和局部变量的速度最快,相反,访问数组元素和对象成员相对较慢。
    • 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快。变量再作用域链中的位置越深,访问所需时间就越长。由于全局变量总处在作用域链的最末端,因此访问速度也是最慢的。
    • 避免使用with语句,因为它会改变执行环境作用域链。同样try-catch语句中的catch子句也有同样的影响,要小心使用。
    • 嵌套的对象成员会明显影响性能,尽量少用。
    • 属性或方法在原型链中的位置越深,访问它的速度也越慢。
    • 可以通过把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善js性能,因为局部变量访问速度更快。
  • 第3章 DOM编程
    • 最小化DOM访问次数,尽可能在js端处理。
    • 如果需要多次访问某个DOM节点,请使用局部变量存储它的引用。
    • 小心处理HTML集合,因为它实时连系着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。
    • 如果可以,使用速度更快的API,如querySelectorAll()和firstElementChild。
    • 要留意重绘和重排;批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数。
    • 动画中使用绝对定位,使用拖放代理。
    • 使用事件委托来减少事件处理器的数量。
  • 第4章 算法和流程控制
    • for\while\do-while循环性能特性相当。
    • 避免使用for-in循环,除非要遍历一个属性数量未知的对象。
    • 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
    • 通常switch比 if-else 快。
    • 在判断条件较多时,使用查找表比 if-else 和switch更快。
    • 浏览器的调用栈大小限制了递归的应用;栈溢出会导致其他代码中断运行。
    • 如果遇到栈溢出,可将递归改为迭代算法,或使用Memoization方法避免重复计算。
  • 第5章 字符串和正则表达式
    • 当连接数量巨大或尺寸巨大的字符串时,数组项合并是唯一在IE7及以下版本中性能合理的方法;如果不需要考虑IE7及以下版本,数组项合并是最慢的字符串连接方法之一,推荐使用简单的+和+=操作符代替,避免不必要的中间字符串。
    • 回溯既是正则匹配功能的基本组成部分,也是正则的低效之源。
    • 回溯失控发生在正则本应快速匹配的地方,但因为某些特殊字符串匹配动作导致运行缓慢甚至浏览器崩溃。避免这个问题的办法是:使相邻字元互斥,避免嵌套量词对同一字符串的相同部分多次匹配,通过重复利用预查的原子组去除不必要的回溯。
    • 提高正则效率的各种技术手段会有助于正则更快地匹配,并在非匹配位置上花更少的时间。
    • 正则并不总是完成工作的最佳工具,尤其当你只搜索字面字符串的时候。
    • 使用2个简单的正则(一个去头一个去尾)来处理大量字符串的去首尾空白能提供一个简洁而跨浏览器的方法。
  • 第6章 快速响应的用户界面
    • 任何js任务都不应当执行超过100毫秒。过长的运行时间会导致UI更新出现明显的延迟。
    • js运行期间,浏览器响应用户交互的行为存在差异。但长时间运行会导致用户体验变得混乱、脱节。
    • 定时器可用来安排代码延迟执行,可以把长时间运行脚本分解成一系列小任务。
    • Web Workers允许在UI线程外部执行js代码,从而避免长时间运行脚本锁定UI。
  • 第7章 Ajax
    • 选择合适的数据传输方式和数据格式。
    • 减少请求树,可通过合并JS和CSS文件,或使用MXHR。
    • 缩短页面的加载时间,页面主要内容加载完成后,用AJAX获取那些次要的文件。
    • 确保你的代码错误不会输出给用户,并在服务端处理错误。
    • 知道何时使用成熟的Ajax类库,以及何时编写自己的底层Ajax代码。
  • 第8章 编程实践
    • 通过避免使用eval()和Function()构造器来避免双重求值带来的性能消耗。同样的,给setTimeout()和setInterval()传递函数而不是字符串作为参数。
    • 尽量使用直接量创建对象和数组。直接量的创建和初始化都比非直接量形式要快。
    • 避免做重复的工作。当需要检测浏览器时,可使用延迟加载或条件预加载。
    • 在进行数学计算时,考虑使用直接操作数字的二进制形式的位运算。
    • 尽量使用原生方法。
  • 第9章 构建并部署高性能js应用
    • 合并js文件以减少http请求数。
    • 使用YUI Compressor压缩js文件。
    • 在服务端压缩js文件(Gzip编码)。
    • 通过正确设置HTTP响应头来缓存js文件,通过向文件名增加时间戳来避免缓存问题。
    • 使用CDN提供js文件;CDN不仅可以提升性能,也为你故那里文件的压缩与缓存。
  • 第10章 工具
    • 使用网络分析工具找出加载脚本和页面中其他资源的瓶颈,这会帮助你决定哪些脚本需要延迟加载,或者需要进一步分析。
    • 尽管传统的经验告诉我们要尽量减少http请求数,但把脚本尽可能延迟加载可以加快页面渲染速度。
    • 使用性能分析工具找出脚本运行过程中速度慢的地方,检查每个函数所消耗的时间,以及函数被调用的次数,通过调用栈自身提供的一些线索来找出需要集中精力优化的地方。
    • 尽管耗费的时间和调用次数通常是数据中最有价值的部分,但仔细观察函数的调用过程,你也许会发现其他优化目标。

第1章 加载和执行

1.推荐将所有<script>标签尽可能放到<body>标签的底部,以尽量减少对整个页面下载的影响。

2.合并脚本:减少页面包含的<script>标签数量,包括外链脚本和内嵌脚本。原因是http请求会带来额外的性能开销。

  • 如何减少script标签数量?可以把多个文件合并成一个,如何合并:
    • 通过离线的打包工具
    • 雅虎的实时在线服务

3.无阻塞的脚本:尽管下载单个较大的js文件只产生一次http请求,却会锁死浏览器一大段时间。为避免这种情况,需要向页面中逐步加载js文件,这样在某种程度上不会阻塞浏览器。无阻塞脚本的秘诀在于,在页面加载完成后才加载js代码,即在window对象的load事件出发后再下载脚本。

  • 如何实现无阻塞

    • 延迟的脚本:script标签有一个defer属性,此属性指明本元素所含的脚本不会修改dom,因此代码能安全地延迟执行。例如:<script src="file.js" defer/>。注意,带有defer属性的script元素是在window.onload执行之前被调用。

    • 动态脚本元素:用js动态创建script元素。重点在于,无论何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。动态创建时放到head标签比body标签里更保险。可以监听script加载完成事件进行下一步操作,此处需要考虑浏览器兼容性。

    • XHR脚本注入:通过XHR对象下载js文件,最后通过创建动态script元素将代码注入页面中。这种方法的优点是,可以下载js代码但不立即执行,可以推迟到需要的时候;浏览器兼容性好。缺点是js文件必须与所请求页面处于相同的域,意味着不能从CDN下载。

    • 推荐做法:先添加动态加载所需代码,然后加载初始化页面所需的剩下的代码。

      <script src="load.js"></script><!--存放loadScript方法定义-->
      <script>
          loadScript("the-rest.js",function(){//处理浏览器兼容性的方法,用于监听脚本加载完成事件
             Application.init(); 
          });
      </script>
      
    • YUI3的方式:由页面中的少量代码来加载丰富的功能组件。

    • LazyLoad类库:开源的无阻塞脚本加载工具。是loadScript函数的增强版,支持下载多个js文件。

    • LABjs:开源的无阻塞脚本加载工具。对加载过程更精细的控制,试图同时下载尽可能多的代码。

第2章 数据存取

1.数据存储位置

1)字面量

字符串、数字、布尔值、对象、数组、函数、正则表达式,和特殊的null和undefined值。

2)本地变量

使用关键字var定义的数据存储单元。

3)数组元素

存储在js数组对象内部,以数字作为索引。

4)对象成员

存储在js对象内部,以字符串作为索引。

2.管理作用域

1)作用域链和标识符解析
2)标识符解析的性能

函数中读写局部变量总是最快的,而读写全局变量通常是最慢的。(采用优化过的js引擎的浏览器,没有类似的性能损失)

一个好的做法是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。

3)改变作用域链

一般来说,一个执行环境的作用域链是不会改变的,但是有2个语句可以在执行时临时改变作用域链:with语句和try-catch的catch子句。with不推荐使用,catch使用得当是推荐的。

4)动态作用域

with、try-catch的catch子句、包含eval()的函数,都是动态作用域。

经过优化的浏览器js引擎,尝试通过分析代码来确定哪些变量可以在特定时候被访问,这些引擎视图避开传统作用域链的查找,取代以标识符索引的方式进行快速查找。设计动态作用域时,这种优化方式就失效了。因此只在必要时才使用动态作用域。

5)闭包、作用域和内存

闭包是js最强大的特性之一,它允许函数访问局部作用域之外的数据。但使用闭包可能会导致性能问题。

问题所在:闭包的属性包含了与执行环境作用域链相同的对象的引用。通常,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在于闭包的实行中,因此集火对象无法被销毁。这意味着闭包需要更多的内存开销。尤其在IE浏览器中需要关注,由于IE使用非原生js对象来实现DOM对象,因此闭包会导致内存泄漏。

下图是一个闭包的作用域链,当调用saveDocument(id)时,需要两次遍历作用域链,第一次从上至下遍历至最后找到saveDocument,第二次从上至下遍历至第2个作用域的最后找到id。因此,若频繁调用,每次都会消耗查找作用域链的时间。

在这里插入图片描述

因此,最好小心地使用闭包,它同时关系到内存和执行速度。不过可以将常用的跨作用域变量存储在局部变量中,然后在闭包内直接访问局部变量来减轻闭包对执行速度的影响。

3.对象成员

对象成员包括属性和方法。

访问对象成员的速度比访问字面量或变量要慢,在某些浏览器中比访问数组元素还要慢。需先理解js中对象的本质。

1)原型

js中的对象是基于原型的。它定义并实现了一个新创建的对象所必须包含的成员列表。原型对象为所有对象实例所共享,因此这些实例也共享了原型对象的成员。

对象通过一个内部属性__proto__绑定到它的原型。一旦你创建一个内置对象(如Object何Array)的实例,它们就会自动拥有一个Object实例作为原型。

因此,对象有2种成员类型:实例成员和原型成员。实例成员直接存在于对象实例中,原型成员从对象原型继承而来。

示例:

var book={
    title: "High Performance JavaScript",
    publisher: "Yahoo! Press"
};

在这里插入图片描述

book.toString()被调用时,会从对象实例开始搜索名为“toString”的成员。一旦book没有名为toString的成员,那么会继续搜索其原型对象,直到找到并执行。

可以使用hasOwnProperty()来判断对象是否包含特定的属性。

2)原型链

可以定义并使用构造函数来创建另一种类型的原型。

示例:

function Book(title,publisher){
    this.title=title;
    this.publisher=publisher;
}
Book.prototype.sayTitle=function(){
    alert(this.title);
};
var book1=new Book("高性能JavaScript","Yahoo! Press");
alert(book1 instanceof Book);//true
book1.sayTitle();//"高性能JavaScript"
alert(book1.toString());//[object Object]

使用构造函数来创建一个新的实例。实例book1的原型__proto__Book.prototype,而Book.prototype的原型是Object。

对象在原型链中存在的位置越深,找到某个属性就越慢。每深入一层原型链都会增加性能损失,搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上遍历原型链带来的开销,这让性能问题更为严重。

3)嵌套成员

对象成员可能包含其他成员。例如window.location.href,每次遇到点操作符,嵌套成员会导致js引擎搜索所有对象成员。

对象成员嵌套得越深,读取速度就会越慢。

执行location.href总是比window.location.href要快,window.location.href也比window.location.href.toString()要快。如果这些属性不是对象的实例属性,那么成员解析还需要搜索原型链,这会花更多的时间。

【大部分浏览器中,通过点表示法(object.name)操作和通过括号表示法(object[‘name’])操作并没有明显的区别,只有在Safari中,点符号始终会更快。】

4)缓存对象成员值

只在必要时使用对象成员。在同一个函数中没有必要多次读取同一个对象成员。

反例:

function hasEitherClass(element,className1,classname2){
    return element.className==className1 || element.className==className2;
}

上述代码中,element.className被读取了2次。

可以将值保存在局部变量中来减少一次查找,因为局部变量的读取速度要快得多,特别是在处理,嵌套对象成员时,这样做会明显提升执行速度。

优化如下:

function hasEitherClass(element,className1,classname2){
    var currentClassName=element.className;
    return currentClassName==className1 || currentClassName==className2;
}

第3章 DOM编程

1.浏览器中的DOM

浏览器中通常会把DOM和JavaScript独立实现。

在IE中,js的实现名为JScript,位于jscript.dll文件中;DOM的实现则存在另一个库中,名为mshtml.dll(内部称为Trident)。这个分离允许其他技术和语言,比如VBScript能共享使用DOM以及Trident提供的渲染函数。

Safari中的DOM和渲染是使用Webkit中的WebCore实现,js部分是由独立的js引擎来实现。

Google Chrome同样使用Webkit中的WebCore库来渲染页面,但js引擎是自己研发的,名为V8。

Firefox的js引擎名为SpiderMonkey,与名为Gecko的渲染引擎相互独立。

2.DOM访问与修改

访问DOM元素是有代价的。修改元素则代价更高,因为它会导致浏览器重新计算页面的几何变化。最坏的情况是在循环中访问或修改元素。因此,通用的经验法则是:减少访问DOM的次数,把运算尽量留在js这一端处理。

1)innerHTML对比DOM方法

修改页面区域的2种方案:innerHTML属性,document.createElement()

在除开最新版的Webkit内核之外的所有浏览器中,innerHTML会更快一些。最新版的Webkit内核浏览器使用document.createElement()更快一些。

使用数组合并字符串会让innerHTML效率更高。

2)节点克隆

使用DOM方法更新页面内容的另一途径是克隆已有元素,而不是创建新元素。即使用element.cloneNode()替代document.createElement()。

3)HTML集合

HTML集合是包含了DOM节点引用的类数组对象。不是真正的数组。没有push或slice等方法,但提供了length属性,并且能以数字索引的方式访问列表中的元素。

以下方法的返回值就是一个HTML集合:

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()

以下属性返回HTML集合:

  • document.images:页面中所有img元素
  • document.links:所有a元素
  • document.forms:所有表单元素
  • document.form[0].elements:页面中第一个表单的所有字段

事实上,HTML集合一直与文档保持着连接,每次你需要最新的信息时,都会重复执行查询的过程,哪怕只是获取即合理的元素个数。这正是低效之源。

循环遍历HTML集合时,可以通过缓存集合的length或拷贝一个HTML集合到普通数组中,再进行循环,执行速度会更快。循环过程中根据索引取集合元素中某一项时,设置局部变量执行速度更快。

3.遍历DOM

1)获取DOM元素

使用childNodes或nextSibling

老版本IE中,nextSibling性能更好。

2)元素节点

大部分现代浏览器提供的API只返回元素节点,推荐使用这些API代替js原生的,因为这些实现过滤的效率要高。

属性名被替代属性是否支持IE8
childrenchildNodes
childElementCountchildNodes.length
firstElementChildfirstChild
lastElementChildlastChild
nextElementSiblingnextSibling
previousElementSiblingpreviousSibling
3)选择器API

除了getElementById和getElementsByTagName,使用CSS选择器也是一种定位节点的便利途径。现代浏览器也提供了一个querySelectorAll的原生DOM方法,这种方式比使用js和dom来遍历查找元素要快得多。

querySelectorAll返回一个NodeList,不是HTML集合,因此不会对应实时的文档结构,避免了HTML集合引起的性能问题。

处理大量组合查询,使用querySelectorAll效率更高。querySelectorAll支持IE8。

querySelector方法返回第一个匹配的节点,同样效率更高。

4.重绘与重排

浏览器下载完页面中的所有内容(HTML、JS、CSS、图片)后会解析并生成2个内部数据结构:DOM树(页面结构)和渲染树(DOM节点如何显示)。

DOM树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM元素再渲染树中没有对应的节点)。一旦DOM和渲染树构建完成,浏览器就开始显示(绘制)页面元素。

渲染树中的节点被称为“帧”或“盒”。

重排:当DOM的变化影响了元素的几何属性(宽和高),浏览器需要重新计算元素的几何属性,同时其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。

重绘:完成重排后,浏览器会重新绘制受影响的部分到屏幕中。

发生重绘不一定发生重排:例如,只改变一个元素的背景色不会影响元素宽高,这时只发生一次重绘,不需要重排,因为元素布局没有改变。

影响性能:重绘和重排都是代价昂贵的操作,它们会导致WEB应用程序的UI反应迟钝,所以应当尽可能减少这类过程的发生。

1)重排何时发生

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

  • 添加或删除可见DOM元素
  • 元素位置改变
  • 元素尺寸改变(margin/padding/border-width/width/height等)
  • 内容改变(例如:文本改变或图片被另一个不同尺寸的图片替代)
  • 页面渲染器初始化
  • 浏览器窗口尺寸改变

有些改变会触发整个页面的重排,例如,当滚动条出现时。

2)渲染树变化的排队与刷新

以下获取布局信息的操作会强制刷新队列并要求计划任务立刻执行:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle(), currentStyle

以上 属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的”待处理变化“并触发重排以返回正确的值。

如果需要多次调用以上属性,尽量避免与会导致重绘、重排的操作穿插调用,放在一起连续调用执行速度更快。

3)最小化重绘和重排

提高程序响应速度的一个策略就是减少重绘和重排操作的发生。为减少发生次数,应该合并多次对DOM和样式的修改,然后依次处理掉。

改变样式

示例:

var el=document.getElementById('mydiv');
el.style.borderLeft='1px';
el.style.borderRight='2px';
el.style.padding='5px';

以上示例中有3个样式属性被改变,每一个都会影响元素的几何结构。最糟糕的情况下会导致浏览器触发3次重排。

大部分现代浏览器为此做了优化,只会触发一次重排,但在旧版浏览器中或者使用计时器时,仍然效率低下。

优化后代码:

//合并改变样式的操作
var el=document.getElementById('mydiv');
el.style.cssText+='border-left:1px;border-right:2px;padding:5px;';

或者也可以直接修改class,而不是修改内联样式。直接改变class的方法更清晰,更易于维护;有助于保持样式与结构分离,但是会带来轻微的性能影响——改变类时需要检查级联样式。

优化代码如下:

var el=document.getElementById('mydiv');
el.className='active';
批量修改DOM

当需要对DOM元素进行一系列操作时,可通过以下步骤减少重绘和重排的次数:

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

该过程里会触发两次重排——第1步和第3步,如果忽略这两个步骤,那么在第2步所产生的任何修改都会触发一次重排。

有3种基本方法可以使DOM脱离文档:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
  • 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

推荐第2种方法,因为使用文档片段所产生的DOM遍历和重排次数最少。

4)缓存布局信息

浏览器尝试通过队列化修改和批量执行的方式最小化重排次数。当你查询布局信息时,比如获取偏移量、滚动位置或计算出的样式值时,浏览器为了返回最新值,会刷新队列并应用所有变更。最好的做法是尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后再操作局部变量。

5)让元素脱离动画流

当页面顶部的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排,让用户感到页面一顿一顿的。渲染树中需要重新计算的节点越多,情况就越糟。

使用以下步骤可避免页面中的大部分重排:

  • 使用绝对位置定位页面上的动画元素,将其脱离文档流。
  • 让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。
  • 当动画结束时恢复定位,从而只会下移一次文档的其他元素。
6)IE和:hover

从IE7开始,IE允许使用:hover这个css伪选择器。但是,如果有大量元素使用了:hover,那么会降低响应速度。这个问题在IE8中更为明显。

5.事件委托

绑定事件处理器性能缺点:

  • 每绑定一个事件处理器都是有代价的,要么是加重了页面负担(更多的标签和js代码),要么是增加了运行期的执行时间。

  • 需要访问和修改的dom元素越多,应用程序也就越慢,特别是事件绑定通常发生在onload时。事件绑定占用了处理时间,浏览器需要跟踪每个事件处理器,这会占用更多的内存。

  • 这些事件处理器中的绝大部分都不再需要,因此有很多工作是没必要的。(不是100%的按钮或链接会被用户点击)

优化方法:事件委托。

事件委托基于一个事实:事件逐层冒泡并能被父级元素捕获。使用事件委托,只需给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。

示例代码:

document.getElementById('menu').onclick=function(e){
    //浏览器target
    e=e||window.event;
    var target=e.target||e.srcElement;
    
    var pageid,hrefparts;
    
    //只关心hrefs,非链接点击则退出
    if(targets.nodeName!=='A'){
        return;
    }
    //从链接中找出页面id
    hrefparts=target.href.split('/');
    pageid=hrefparts[hrefparts.length-1];
    pageid=pageid.replace('.html','');
    //更新页面
    ajaxRequest('xhr.php?page='+id,updatePageContents);
    //浏览器阻止默认行为并取消冒泡
    if(typeof e.preventDefault === 'function'){
        e.preventDefault();
        e.stopPropagation();
    }else{
        e.returnValue=false;
        e.cancelBubble=true;
    }
}

第4章 算法和流程控制

代码组织结构和解决具体问题的思路是影响代码性能的主要因素。

1.循环

1)循环的4种类型
  • 标准for循环:

    • 由4部分组成:初始化、前测条件、后执行体、循环体。
  • while循环:

    • 由2部分组成:前测条件、循环体。
  • do-while循环:

    • 唯一一种后测循环,由2部分组成:循环体、后测条件。
  • for-in循环:

    • 可以枚举任何对象的属性名。所遍历的属性包括对象实例属性以及从原型链中继承而来的属性。
2)循环性能
for-in

for-in比其他三种明显慢。因为每次遍历都会同时搜索实例或原型属性,会产生更多开销。对比其他方式,for-in最终只有其他方式速度的1/7。所以尽量避免使用for-in循环。

可以通过取出对象的属性列表,遍历属性列表,减少for-in的开销。优化代码如下:

var props=["prop1","prop2"],i=0;
while(i<props.length){
    process(object[props[i++]]);
}

其他三种方式性能差不多。需要根据情况选择:

  • 根据每次迭代处理的事务
  • 根据迭代的次数

通过减少这两者中的一个或全部的时间开销,就能提升循环的整体性能。

减少迭代的工作量

循环复杂度为O(n)时,减少迭代工作量是最有效的方法。

  • 获取数组长度并缓存到局部变量中,避免每次迭代都获取一次:
for(var i=0,len=items.length;i<len;i++){
    process(items[i]);
}

根据数组长度,在大部分浏览器中能节省大概25%的运行时间。

  • 倒序循环
for(var i=items.length;i--;){
    process(items[i]);
}

把减法操作放在控制条件中。现在每个控制条件只是简单地与0比较。控制条件与true值比较时,任何非零数会自动转换为true,而0值等同于false。

现在控制条件已经从2次比较(迭代数小于总数吗?它是否为true?)减少到1次比较(它是true吗?)。每次迭代从2次比较减少到1次,运行速度快了50%-60%。

减少迭代次数

复杂度大于O(n)时,建议着重减少迭代次数。

达夫设备:限制循环迭代次数的模式。是一个循环体展开技术,使得一次迭代中实际上执行了多次迭代的操作。

达夫设备的一个典型实现如下:

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);

基本理念:每次循环最多可调用8次process()。循环的迭代次数为总数除以8.由于不是所有数字都能被8整除,变量startAt用来存放余数,表示第一次循环中应调用多少次process()。如果是12次,那么第一次循环会调用process()4次,第二次循环调用process()8次,用2次循环替代了12次循环。

此算法一个稍快的版本取消了switch语句,并将余数处理和主循环分开:

var i=items.length%8;
while(i){
    process(items[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--]);
}

这种方式用2次循环代替之前的1次循环,但它移除了switch语句,速度比之前的更快。

但是达夫设备只在迭代数超过1000的情况下有明显效率的提升。例如在500 000次迭代中,其运行时间比常规循环少70%。

3)基于函数的迭代

forEach()

使用方便,但比基于循环的迭代要慢一些。基于循环的迭代比基于函数的迭代快8倍。

性能消耗:对每个数组项调用外部方法带来的开销是速度慢的主要原因。

2.条件语句

1)if-else对比switch

基于测试条件的数量判断使用哪种:条件数量越大,越倾向于使用switch而不是if-else。——基于代码的易读性。

事实证明,当条件数量很大时,switch比if-else运行得要快。

这两个语句的性能区别是:当条件增加时,if-else性能负担增加的程度比switch要多。——基于性能。

通常来说,if-else适用于判断两个离散值或几个不同的值域。当判断多于2个离散值时,switch语句更好。

2)优化if-else

优化目标:最小化到达正确分支前所需判断的条件数量。

优化策略:

  • 最简单方法:条件语句总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。
  • 把if-else组织成一系列嵌套的if-else语句,会使运行速度平均化。
3)查找表

当有大量离散值需要测试时,if-else和switch比使用查找表慢很多。

查找表方法:使用数组和普通对象来构建查找表。

在这里插入图片描述
优点:

  • 速度更快
  • 代码可读性更好,不用书写任何条件判断语句
  • 候选值数量增加时,几乎不会产生额外的性能开销

查找表示例:

var results=[result0,result1,result2,result3,result4,result5];
return results[value];

switch更适合于每个键都需要对应一个独特的动作或一系列动作的情况。

3.递归

使用递归可以把复杂的算法变得简单。

递归函数的潜在问题是终止条件不明确或缺少终止条件会导致函数长时间运行,并使得用户界面处于假死状态。还可能遇到浏览器的调用栈大小限制(Call stack size limites)。

1)调用栈限制

调用栈限制大小与浏览器关系:除IE外的浏览器都有固定数量的调用栈限制,IE的调用栈限制大小与系统空闲内存有关。大多数现代浏览器的调用栈限制大小比老版本浏览器高很多。

当递归次数超过最大调用栈容量时,浏览器会报告以下出错信息

  • IE: Stack overflow at line x;同时弹出alert警告提示栈溢出
  • Firefox: Too much recursion
  • Safari: Maximum call stack size exceeded
  • Opera: Abort(control stack overflow)
  • Chrome: 不显示

栈溢出可通过try-catch捕获处理。

2)递归模式

2种递归模式:

  • 直接递归模式:1个函数递归
  • 隐伏模式:2个函数相互调用,行程无限循环,较难定位原因

隐伏模式示例:

//2个函数
function first(){
    second();
}
function second(){
    first();
}
first();

最常见的导致栈溢出的原因是不正确的终止条件。因此定位模式错误的第一步是验证终止条件。如果终止条件没问题,那么可能是算法中包含了太多层递归。

建议改用迭代、Memoization。

3)迭代

任何递归能实现的算法,同样可以用迭代来实现。迭代算法通常包含几个不同的循环,这会引入自身的性能问题。

使用优化后的循环替代递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少很多。

4)Memoization

是一种避免重复工作的方法,它缓存前一个计算结果供后续计算使用。

多次调用递归函数时,大量重复工作不可避免。

Memoization的关键在于在函数内部创建一个缓存对象,将缓存中没有的计算结果放到缓存中,多次调用时已计算的结果就可以直接从缓存取出。

以下是封装了基础功能的memoize()函数:

function memoize(fundamental,cache){
    //fundamental为需要增加缓存功能的原递归函数;cache为可选的缓存对象
    cache=cache||{};
    var shell=function(arg){
        if(!cache.hasOwnProperty(arg)){
            cache[arg]=fundamental(arg);
        }
        return cache[arg];
    };
    return shell;
}

第5章 字符串和正则表达式

1.字符串连接

字符串合并的方法:

  • str=“a”+“b”;

  • str=“a”;

    str+=“b”;

  • str=[“a”,“b].join(”");

  • str=“a”;

    str=str.concat(“b”);

当连接少量字符串时,这些方法运行速度都很快。当需要合并的字符串长度和数量增加,有一些方法开始展现优势。

1)+ 和 += 操作符

举例:

str+="one"+"two";

此代码运行时,会经历4个步骤:

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

优化代码1

用2行语句直接附加内容给str,避免了产生临时字符串(原第1步和第2步)。大多数浏览器中这样优化会提速10%-40%。

str+="one";
str+="two";

优化代码2

赋值表达式由str开始作为基础,每次给它附加一个字符串,由左向右一次连接,因此避免了使用临时字符串。

如果改变连接顺序(如:str=“one”+str+“two”),本优化将会失效。这与浏览器合并字符串时分配内存的方法有关:

  • 除IE外,其他浏览器会尝试为表达式左侧的字符串分配更多的内存,然后将第2个字符串拷贝至它的末尾。如果在一个循环中,基础字符串位于最左端的位置,就可以避免重复拷贝一个逐渐变大的基础字符串。

  • IE8与其他浏览器较相似。IE7及以下版本,使用优化代码2会更慢。因为在与一个唱的基础字符串合并前,先连接多个短字符串会使速度更快(避免了多次复制大字符串)。

    例如largeStr=largeStr+s1+s2,IE7及以下版本中,必须将这个长字符串拷贝2次,1次是与s1合并,1次是与s2合并。相反,largeStr+=s1+s2,首先将2个小字符串合并起来,然后将结果再返回给长字符串。创建字符串s1+s2与两次拷贝字符串相比,性能影响要小得多。

    这是由IE执行连接操作的底层机制决定的。

    • 在IE8中,连接字符串只是记录现有的字符串的引用来构造新字符串。在最后时刻(调用时),字符串的各个部分才会逐个拷贝到一个新的“真正的”字符串中,然后用它取代先前的字符串引用。所以并非每次使用字符串时都发生合并操作。
    • IE7及以下版本,使用了更糟糕的实现方法:每连接一对字符串都要把它复制到一块新分配的内存中。

【基本字符串】:连接时排在前面的字符串。str+“one"意味着拷贝"one"并附加在str之后,而"one”+str则意味着要拷贝str并附加在"one"之后。如果str很大,则拷贝过程的内存占用就大。

str=str+"one"+"two";
//等价于str+((str+"one")+"two");
2)Firefox和编译期合并

在赋值表达式中所要链接的字符串都属于编译期常量。

Firefox会在编译过程中自动合并编译期常量。

str="one"+"two";在Firefox中会自动变成str="onetwo";

这种方式由于运行期没有中间字符串,所以花在连接过程的时间和内存可以减少到0。但是这种做法不常用到,因为更多时候是用运行期的数据构建字符串,而不是常量的拼接。

3)数组项合并

Array.prototype.join方法

大多数浏览器中,使用join合并比其他字符串连接方法更慢,但在IE7及以下版本中合并大量字符串的情况是高效的。

在IE7中使用join避免了重复分配内存和拷贝逐渐增大的字符串。当把数组所有元素连接在一起时,浏览器会分配足够的内存来存放整个字符串,而且不会多次拷贝最终字符串中相同的部分。

4)String.prototype.concat

使用concat比+和+=稍慢,尤其是在IE、Opera和Chrome中更慢。有潜伏的灾难性的性能问题(随数据量的增大,具有指数级别倍数的慢速度)。

2.正则表达式优化

两个正则表达式匹配相同的文本并不意味着有着相同的速度。部分匹配比完全不匹配所用的时间要长。不同浏览器对正则表达式引擎有着不同程度的内部优化。

1)正则表达式工作原理

处理一个正则表达式的基本步骤:

  1. 编译:创建一个正则表达式对象(使用字面量或RegExp构造函数),浏览器会验证你的表达式,然后把它转化为一个原生代码程序,用于执行匹配工作。如果把正则对象赋值给一个变量,可以避免重复执行这一步骤。

  2. 设置起始位置:当正则类进入使用状态,首先要确定目标字符串的起始搜索位置。这是字符串的起始字符,或者由正则表达式的lastIndex属性指定。当尝试匹配失败时,此位置则在最后一次匹配的起始位置的下一位字符的位置上。

    浏览器优化正则的办法是:提前决定跳过一些不必要的步骤,来避免大量无意义的工作。

  3. 匹配每个正则表达式字元:一旦正则表达式知道开始位置,它会逐个检查文本和正则表达式模式。当一个特定的字元匹配失败时,正则表达式会试着回溯到之前尝试匹配的位置上,然后尝试其他可能的路径。

  4. 匹配成功或失败:如果在字符串当前的位置发现了一个完全匹配,那么正则表达式宣布匹配成功。如果在所有可能的路径都没有匹配到,正则表达式引擎会回退到第二步,然后从下一个字符重新尝试。当每个字符都经理这个过程,还没有成功匹配,那么就匹配失败。

2)回溯

回溯是正则表达式匹配过程中的基础组成部分。但是回溯会产生昂贵的计算消耗。理解它的工作原理以及如何最少化地使用它,是编写高效正则的关键。

回溯过程概述:

  • 当正则匹配目标字符串时,它从左到右逐个测试表达式的组成部分,看是否能找到匹配项。

  • 在遇到量词(比如*,+?或{2,0})和分支(|操作符)时,需要决策下一步如何处理。遇到量词需决定何时尝试匹配更多字符;遇到分支则必须从可选项中选择一个尝试匹配。

  • 每当正则表达式做类似的决定时,如果有必要都会记录其他选择,以备返回时使用。

  • 如果当前选项匹配成功,正则表达式继续扫描表达式;如果其他部分也匹配成功,那么匹配结束。

  • 但是,如果当前选项找不到匹配值,或后面的部分匹配失败,那么正则表达式会回溯到最后一个决策点,然后在剩余的选项中选择一个;这个过程会一直进行,直到找到匹配项或者正则表达式中量词和分支选项的所有排列组合都尝试失败,那么它将放弃匹配,转而移动到字符串中的下一个字符,再重复此过程。

3)回溯失控

当正则表达式导致浏览器假死数秒、数分钟、甚至更长时间,问题很可能是回溯失控。

是由过多贪婪量词(.*?[\s\S]*?)导致的反复回溯。

解决方案:尽可能具体化分隔符之间的字符串匹配模式。比如改为".*?"用来匹配由双引号包围的字符串。

匹配HTML字符串

匹配HTML字符串时,可以通过重复一个非捕获组来实现,它包含了否定性预查(阻止下一个依赖标签)和[\s\S](任意字符)元序列。非捕获组例如=>(?:(?!<head>)[\s\S])*<head>

这样消除了潜在的回溯失控,并允许正则表达式在匹配不完整的HTML字符串失败时所需要时间通字符串长度成线性关系。但是为每个匹配字符重复预查是严重缺乏效率的。这种方法在匹配短字符串时运行良好。

使用预查和反向引用的模拟原子组

原子组:(?>…),其中…表示任意正则表达式。它是一种具有特殊反转性的非捕获组。一个原子组中存在一个正则,该组的任何回溯位置都会被丢弃。可以有效阻止海量回溯。

模拟原子组:js不支持原子组,可以利用预查((?=...))过程来模拟。预查作为全局匹配的一部分,并不消耗任何字符;只是检查自己包含的正则符号在当前字符串位置是否匹配。可以通过把预查的表达式封装在捕获组中并给它添加一个反向引用的方法来避免这一问题。

模拟原子组示例:

(?=(...))\1

匹配HTML字符串应用:(?=([\s\S]*?<head>))\1

嵌套量词和回溯失控

嵌套量词是指量词出现在一个自身被重复量词修饰的组中,例如(x+)*

4)基准测试的说明

建议总是用包含特殊匹配的长字符串来测试正则表达式。

5)更多提高正则表达式效率的方法
  • 关注如何让匹配更快失败:因为正则匹配失败的位置比匹配成功的位置要多得多。
  • 正则表达式以简单、必需的字元开始:起始标记应尽可能快速地测试并排除明显不匹配的位置。
  • 使用量词模式,是它们后面的字元互斥:尽量具体化匹配模式。
  • 减少分支数量,缩小分支范围:可以通过字符集合选项组件来减少对分支的需求;当分支必不可少时,将常用分支放到最前面。
  • 使用非捕获组:捕获组消耗时间和内存来记录反向引用。
  • 只捕获感兴趣的文本以减少后处理。
  • 暴露必需的字元。
  • 使用合适的量词。
  • 把正则表达式赋值给变量并重用它们:可以避免对正则重新编译。
  • 将复杂的正则拆分为简单的片段。
6)何时不使用正则

字符串方法(charAt/slice/substr/substring)都可用在特定位置上提取并检查字符串的值。

indexOf和lastIndexOf方法非常适合查找特定字符串的位置,或判断是否存在。

所有的字符串方法速度都很快,当搜索那些并不依赖正则复杂特性的字面字符串时,字符串方法有助于避免正则带来的性能开销。

3.去除字符串收尾空白

trim()

1)使用正则表达式去首尾空白

使用2个子表达式:去除头部的空白、去除尾部的空白。

if(!String.prototype.trim){
    String.prototype.trim=function(){
        return this.replace(/^\s+/,"").replace(/\s+$/,"");
    }
}
2)不使用正则表达式去除字符串首尾空白
String.prototype.trim=function(){
    var start=0,
        end=this.length-1,
        ws="\n\r\t\f\xob\xa0\u168o\u18oe\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff";
    while(ws.indexOf(this.charAt(start))>-1){
        start++;
    }
    while(end>start&&ws.indexOf(this.charAt(end))>-1){
        end--;
    }
    return this.slice(start,end+1);
}

ws变量包含了es5定义的所有空白字符。

缺点:不宜用来处理前后大段的空白字符。因为通过循环遍历字符串来确定空白字符的效率比不上正则表达式使用的优化后的搜索代码。

3)混合解决方案

用正则过滤头部空白,用非正则方法过滤尾部字符。

String.prototype.trim=function(){
    var str=this.replace(/^\s+/,""),
        end=str.length-1,
        ws=/\s/;
    while(ws.test(str.charAt(end))){
        end--;
    }
    return str.slice(0,end+1);
}

第6章 快速响应的用户界面

浏览器让一个单线程共用于执行js和更新用户界面。每个时刻只能执行其中一种操作,当js代码执行时,用户界面处于“锁定”状态。

1.浏览器UI线程

浏览器UI线程:用于执行JS和更新用户界面的进程。

UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些任务要么是运行js代码,要么是执行UI更新,包括重绘和重排。

大多数浏览器在js运行时会停止把新任务加入UI线程的队列中。

1)浏览器限制

浏览器限制分2种:调用栈大小限制和长时间运行脚本限制。

长时间运行脚本限制基本原理是浏览器会记录一个脚本的运行时间,并在达到一定限度时终止它。

2种方法可以度量脚本运行时间:

  • 记录自脚本开始以来执行语句的数量。
  • 记录脚本执行的总时长。

不同浏览器检测方法和具体限制大小不同:

  • IE4及以上,设置默认限制为500万条语句;此限制放在Windows注册表中,叫做HKEY_CURRENT_USER\Software\Microsoft\InternetExplorer\Styles\MaxScriptStatements。
  • Firefox的默认限制为10秒;该限制记录在浏览器配置设置中,键名为dom.max_script_time。
  • Safari的默认限制为5秒;该限制无法更改,但是可以通过Develop菜单选择Disable Runaway JavaScript Timer来禁用定时器。
  • Chrome没有单独的长运行脚本限制,替代做法是依赖其通用崩溃检测系统来处理此类问题。
  • Opera没有长运行脚本限制,它会继续执行js代码直到结束,鉴于Opera架构,脚本运行结束时不会导致系统不稳定。

当长运行脚本限制被触发时,浏览器会弹出对话框提示用户。这个代码无法检测,也不能改变对话框外观,所以只能尽量避免该问题发生。

2)多久算长时间

单个js操作花费的总时间不应该超过100毫秒。

Nielsen指出如果界面在100毫秒内响应用户输入,用户会认为自己在“直接操纵界面中的对象”。超过100毫秒意味着用户会感到自己与界面失去联系。

建议不要让任何js代码持续运行50毫秒以上。

2.使用定时器让出时间片段

难免会有一些复杂的js任务不能在100毫秒内完成,最理想的方法是让出UI线程的控制权,使得UI可以更新。

1)定时器基础

setTimeout() setInterval()

方法有2个参数,第一个是函数,第2个是毫秒数。但第二个时间表示任务何时被添加到UI队列,而不是一定会在这段时间后执行,这个任务会等待队列中其他所有任务执行完毕才会执行。

无论发生何种情况,创建一个定时器会造成UI线程暂停。因此,定时器代码会重置所有相关的浏览器限制,包括长时间运行脚本定时器。此外,调用栈也在定时器的代码中重置为0。这一特性使得定时器成为长时间运行js代码理想的跨浏览器解决方案。

2)定时器精度

js定时器延迟通常不太精准,相差大约几毫秒。指定定时器延时250毫秒,并不意味着任务会在调用setTimeout之后过250毫秒时精准地加入队列。浏览器都会发生几毫秒偏移,或快或慢。因此,定时器不可用于测量实际时间。

在Windows系统中定时器分辨率为15毫秒,也就是说一个延时15毫秒的定时器将根据最后一次系统时间刷新而转换为0或15。设置定时器延时小于15将会导致IE锁定,所以延迟的最小值建议为25毫秒(实际时间是15或30),以确保至少有15毫秒延迟。

定时器延时的最小值有助于避免在其他浏览器和其他操作系统中的定时器出现分辨率问题。大多数浏览器在定时器延时等于或小于10毫秒时表现不太一致。

3)使用定时器处理数组

常见的一种长运行脚本的起因是耗时过长的循环。可以使用定时器优化——基本方法是把循环的工作分解到一系列定时器中。

影响循环性能的2部分:数组长度长;处理语句复杂。

决定是否可以使用定时器取代循环的2个因素:

  • 处理过程是否必须同步?
  • 数据是否必须按顺序处理?

如果都是否,那么可以使用定时器取代。

//原循环
for(var i=0,len=items.length;i<len;i++){
    process(items[i]);
}
//用异步取代循环,封装一个函数
function processArray(items,process,callback){
	var todo=items.concat();//克隆原数组
	setTimeout(function(){
    	process(todo.shift());
    	//如果还有需要处理的元素,创建另一个定时器
    	if(todo.length>0){
        	setTimeout(arguments.callee, 25);//因为下一个定时器需要运行相同的代码,所以第一个参数为arguments.callee,该值指向当前正在运行的匿名函数
    	}else{
        	callback(items);
    	}
	},25);
}

使用定时器取代循环缺点:处理数组的总时长增加了。这是因为在每一个条目处理完成后UI线程会空闲出来,并且在下一条目开始处理之前会有一段延时。

尽管如此,为避免锁定浏览器给用户带来的糟糕体验,这种取舍是有必要的。

4)分割任务

如果一个函数运行时间太长,可以把它拆分成一系列更小的步骤,把每个独立的方法放在定时器中调用。

5)记录代码运行时间

批量处理比单个处理要快。

可以通过原生的Date对象来跟踪代码的运行时间,这是大多数js分析工具的工作原理。

通过添加一个时间监测机制来改进processArray()方法,使得每个定时器能处理多个数组条目:

function timedProcessArray(items,process,callback){
    var todo=items.concat();//克隆原数组
	setTimeout(function(){
        var start=+new Date();
        //每个数组条目处理完后检测执行时间
        do{
            process(todo.shift());
        }while(todo.length>0&&(+new Date()-start<50));

    	//如果还有需要处理的元素,创建另一个定时器
    	if(todo.length>0){
        	setTimeout(arguments.callee, 25);
    	}else{
        	callback(items);
    	}
	},25);
}

使用50毫秒内的任务批量处理,能避免把任务分解成过于零碎的片段。

6)定时器与性能

过度使用定时器会对性能造成负面影响。以上小节的代码使用了定时器序列,同一时间只有一个定时器存在,只有当这个定时器结束时才会新创建一个。这种方法使用定时器不会导致性能问题。

当多个重复的定时器同时创建往往会出现性能问题。因为只有一个UI线程,而所有的定时器都在争夺运行时间。那些间隔在1秒或以上的低频率重复定时器几乎不会影响Web应用响应速度;而多个重复定时器使用较高的频率(100-200毫秒之间)时,应用会明显变慢。

3.Web Workers

Web Workers给Web应用带来潜在的巨大性能提升,因为每个Worker都在自己的线程中运行代码。这意味着Worker运行代码不仅不会影响浏览器UI,也不会影响其他Worker中运行的代码。

1)Worker运行环境

由于Web Workers没有绑定UI线程,它们不能访问浏览器的资源。

Web Workers从外部线程中修改DOM会导致用户界面出现错误。

每个Web Worker都有自己的全局运行环境,由如下部分组成:

  • navigator对象,包括4个属性:appName、appVersion、userAgent、platform。
  • location对象,与window.location相同,但所有属性都是只读的。
  • self对象,指向全局worker对象。
  • importScripts()方法:加载Worker所用到的外部js文件。
  • 所有js对象,如Object、Array、Date等。
  • XMLHttpRequest构造器。
  • setTimeout()和setInterval()方法。
  • close()方法:能立刻停止Worker运行。

需要创建一个完全独立的js文件,其中包含了需要在Worker中运行的代码。创建Web Worker必须传入这个js文件的url:var worker=new Worker("code.js");

此代码一旦执行,将为这个文件创建一个新的线程和一个新的Worker运行环境。该文件会被异步下载,直到文件下载并执行完成后才会启动此Worker。

2)与Worker通信

Worker与网页代码通过事件接口进行通信。网页代码可以通过postMessage()方法给Worker传递数据,它接受一个参数,即需要传递给Worker的数据。此外,Worker还有一个用来接受信息的onmessage事件处理器。

消息系统是网页和Worker通信的唯一途径。

3)加载外部文件

Worker通过importScripts()方法加载外部js文件,该方法接收一个或多个js文件的url作为参数,该方法的调用过程是阻塞式的,直到所有文件加载并执行完成后,脚本才会继续运行。由于Worker在UI线程之外运行,所以这种阻塞并不会影响UI响应。

4)实际应用

Web Workers适用于处理纯数据,或者与浏览器UI无关的长时间运行脚本。

可能受益的情况:

  • 编码/解码大字符串
  • 复杂数学运算(包括图像或视频处理)
  • 大数组排序

任何超过100毫秒的处理过程,都应当考虑Worker方案是不是比基于定时器的方案更为合适,前提是浏览器支持Web Workers。

第7章 Ajax

ajax可以通过延迟下载资源文件使页面加载速度更快。通过异步的方式在客户端和服务端之间传输数据,从而避免页面资源一窝蜂地下载。选择适合的传输方式和数据格式,可以显著改善用户和网站的交互体验。

1.数据传输

ajax是一种与服务器通信而无需重载页面的方法。

1)请求数据

有5种常用技术用于请求:

  • XHR
  • 动态脚本注入
  • iframes
  • Comet
  • Multipart XHR

现代高性能js中使用的3种技术是:XHR、动态脚本注入、Multipart XHR。

HTTP请求是ajax中最大的瓶颈之一,因此减少http请求数量能提高页面性能。

2)发送数据

当数据只需要发送到服务器时,有2种广泛使用的技术:XHR和信标。

使用XHR发送数据到服务器时,GET方式会更快,因为对于少量数据而言,一个GET请求网服务器只发送一个数据包,而一个POST请求至少发送2个数据包,一个装载头信息,另一个装载POST正文。

信标(Beacons):类似动态脚本注入。可以使用图片信标,创建一个img元素但不插入页面DOM,给img的src设置为请求服务器上脚本的URL,URL上可以传参。

信标优点:无需向客户端发送任何回馈信息;性能消耗小;服务端的错误不会影响到客户端。

信标缺点:无法发送POST数据;发送的数据长度被限制得很小。

选择数据传输技术需考虑的因素:功能集、兼容性、性能、方向。

2.数据格式

考虑数据格式时,唯一需要比较的标准就是速度。

1)XML

优点:极佳的通用性、格式严格易于验证。

缺点:冗长,每个单独的数据片段都依赖大量结构,所以有效数据的比例非常低;js程序手动解析复杂。

2)JSON

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

JSON-P:在使用动态脚本注入时,JSON数据被当成一个js文件并作为原生代码执行。为实现这一点,这些数据必须封装在一个回调函数里。这就是JSON填充(JSON with padding)或 JSON-P。

优点:体积小;在相应信息中结构所占的比例更小,数据占用比例更多;极好的通用性;客户端解析速度快。

3)HTML

可以在服务端构建好整个HTML再传回客户端,js可以通过innerHTML属性把HTML插入页面相应位置。

缺点:数据格式臃肿;传输数据量大;客户端解析速度慢。

当客户端的瓶颈是CPU而不是带宽时可以使用此数据格式。

4)自定义格式

把数据用分隔符连接起来,返回一个字符串,js用split分割。

优点:快速下载;易于解析;格式简洁,数据占比高。

3.Ajax性能指南

除数据传输技术和数据格式以外的其他优化技术。

1)缓存数据

2种方法避免发送不必要的请求:

  • 在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存
  • 在客户端,把获取到的信息存储到本地
设置HTTP头信息

使用GET方式,设置Expires头信息。

本地数据存储

前端代码请求时,若缓存中无对应数据则请求,请求后保存到全局变量localCache中,若缓存中已有则从缓存中获取数据。

2)Ajax类库的局限

类库一般不提供readystatechange事件,无法使用通过监听readyState为3时使用multipart XHR。

第8章 编程实践

1.避免双重求值

当你在js代码中执行另一段js代码时,都会导致双重求值的性能消耗。此代码首先会以正常的方式求值,然后在执行过程中对包含于字符串中的代码发起另一个求值运算。

使用eval()、Funtion()、setTimeout()、setInterval()时都要创建一个新的解释器/编译器实例。这使得消耗很大,代码执行速度变慢。

2.使用Object/Array直接量

直接量比new Object()new Array()运行得更快,并且可以节省代码量。

3.避免重复工作

最常见的重复工作就是浏览器探测。

有几种方法可以避免。

1)延迟加载

延迟加载意味着在信息被使用前不会做任何操作。

在第一次调用时,会先检查浏览器兼容性并决定使用哪个api,然后用新函数覆盖原函数。这样的话随后每次调用都不会在做浏览器检测。

当一个函数在页面中不会立刻调用时,延迟加载是最好的选择。

2)条件预加载

条件预加载会在脚本加载期间提前检测,而不会等到函数被调用。检测的操作依然只有一次。

预加载适用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现。

4.使用速度快的部分

js引擎是由低级语言构建的而且经过编译,所以引擎是很快的。但js代码运行速度慢。引擎的某些部分允许你绕过那些慢的部分。

1)位操作

js中的数字都依照IEEE-754标准以64位格式存储。在位操作中,数字被转换为有符号的32位格式。每次运算符会直接操作该32位数以得到结果。尽管需要转换,但这个过程与js其他数学运算和布尔操作相比要快很多。

按位与、按位或、按位异或、按位取反

有好几种方法利用位操作符提升js的速度——使用位运算代替纯数学操作:

  1. 比如通常采用对2取模运算实现表格行颜色交替:
//原写法
for(var i=0,len=rows.length;i<len;i++){
    if(i%2){
        className="even";
    }else{
        className="odd";
    }
    //增加class
}
//使用位操作符优化
for(var i=0,len=rows.length;i<len;i++){
    if(i&1){
        className="odd";
    }else{
        className="even";
    }
    //增加class
}

32位数字的二进制底层表示,可以发现偶数的最低位是0,奇数的最低位是1。可以简单地通过让给定数与数字1进行按位与运算判断。当次数为偶数,那么它和1进行按位与运算结果是0,如果此数为奇数,那么它和1进行按位与运算的结果就是1。

虽然代码改动不大,但优化后比原来快了50%。

  1. 使用位掩码

位掩码用于处理同时存在多个布尔选项的情形。其思路:使用单个数字的每一位来判定是否选项成立,从而有效地把数字转换为由布尔值标记组成的数组。掩码中的每个选项的值都等于2的幂。

举例:

var OPTION_A=1;
var OPTION_B=2;
var OPTION_C=4;
var OPTION_D=8;
var OPTION_E=16;

var options=OPTION_A|OPTION_C|OPTION_D;
//接下来可以通过按位与操作来判断一个给定的选项是否可用:如果该选项未设置则运算结果为0;如果已设置则结果为1
if(options&OPTION_A){//选项A是否在列表中
    //do sth.
}
if(options&OPTION_B){//选项B是否在列表中
    //do sth.
}

这样的位掩码运算速度非常快,原因是计算操作发生在系统底层。如果有许多选项保存在一起并频繁检查,位掩码有助于提高整体性能。

2)原生方法

无论js代码如何优化,都永远不会比js引擎提供的原生方法更快。因为js的原声部分在你写代码前已经存在浏览器中了,并且都使用低级语言写的。这意味着这些原生方法会被编译成机器码,成为浏览器的一部分。

  1. 复杂的数学运算使用原生的Math对象性能更好。Math对象包含一些常见的数学常量和api。
  2. jquery引擎被广泛认为是最快的css查询引擎,但是它仍然比原生方法慢。原生的querySelector()和querySelectorAll()方法完成任务平均所需时间是基于js的css查询的10%。

当原生方法可用时尽量使用它们,特别是数学运算和DOM操作。

第9章 构建并部署高性能js应用

1.Apache Ant

Apache Ant是一个软件构建自动化工具。用Java实现并用XML描述构建过程。

2.Gulp

比起Apache ant,Gulp更为流行。

3.合并多个js文件

4.预处理js文件

5.js压缩

js压缩是指把js文件中所有与运行无关的部分进行剥离的过程。玻璃的内容包括注释和不必要的空白字符。该过程通常可以将文件大小减半,促使文件更快被下载。

YUI Compressor提供了更高级别的压缩,除了压缩注释和不必要的空格,还提供如下功能:

  • 将局部变量替换成更短的形式(1个、2个或3个字符),以优化后续的Gzip压缩工作。
  • 尽可能将方括号表示法替换为点表示法(如foo['bar']被替换成foo.bar)。
  • 尽可能去掉直接量属性名的引号(如{"foo":"bar"}被替换成{foo:"bar"})。
  • 替换字符串中的转义符号(如'aaa\'bbb'被替换成"aaa'bbb")。
  • 合并常量(如"foo"+"bar"被替换成"foobar")。

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

开发高性能应用的一个普遍规则是,只要是能在构建时完成的工作,就不要留到运行时去做。

7.js的HTTP压缩

Accept-Encoding头信息用于告诉Web服务器它支持哪种编码转换类型。这个信息主要用来压缩文档以获得更快的下载。可用的值包括:gzip、compress、deflate、identity。

如果Web服务器在请求中看到这些信息头,它会选择最适合的编码方法,并通过Content-Encoding头信息通知Web浏览器它的决定。

gzip是目前最流行的编码方式,它通常能减少70%的下载量。gzip压缩主要适用于文本(包括js文件)。其他类型比如图片、pdf文件,不应该使用gzip,因为他们本身已经被压缩过,试图重复压缩只会浪费服务器资源。

8.缓存js文件

  • Web服务器通过“Expires HTTP响应头”来告诉客户端一个资源应当缓存多长时间。

  • 使用HTML5离线应用缓存。

9.处理缓存问题

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

10.使用内容分发网络(CDN)

CDN是互联网上按地理位置分布计算机网络,它负责传递内容给终端用户。使用CDN的主要原因是增强Web应用的可靠性、可扩展性,更重要的是提升性能。事实上,通过地理位置最近的用户传输内容,CDN能极大地减少网络延时。

11.部署js资源

部署js资源的过程通常需要复制大量文件到一台或多台远程主机,有时还需要在远程主机上执行一系列的shell命令,尤其是使用CDN时,需要通过传输网络分发新添加的文件。

Apache Ant提供几个选项用于复制文件到远程主机。

为了在运行SSH守护进程的远程主机上执行shell命令,可以使用可选的SSHEXEC任务或直接调用ssh工具。

12.敏捷js构建过程

传统的build工具很优秀,但是每一次变更代码后都必须手动编译。Web开发人员更中意的方案是跳过编译的步骤,只需刷新浏览器窗口即可。

smasher是个php5编写的结合上述先进技术的工具,能帮助Web开发人员在高效工作的同事依然能获得应用最佳性能。

第10章 工具

性能分析:在脚本运行期间定时执行各种函数和操作,找出需要优化的部分。

网络分析:检查图片、样式表和脚本的加载过程,以及它们对页面整体加载和渲染的影响。

1.js性能分析

使用Date对象测量脚本的任何部分。

比起手动插入计时代码,一个能处理时间计算并存储数据的Timer对象会是更好的方案:

var Timer={
    _data:{},
    start:function(key){
        Timer._data[key]=new Date();
    },
    stop:function(key){
        var time=Timer._data[key];
        if(time){
            Timer._data[key]=new Date()-time;
        }
    },
    getTime:function(key){
        return Timer._data[key];
    }
};

//使用
Timer.start('createElement');
for(i=0;i<count;i++){
    element=document.createElement('div');
}
Timer.stop('createElement');
alert(Timer.getTime('createElement'));

2.YUI Profiler

是一个用js编写的js性能分析工具。除了计时功能,还提供了针对函数、对象和构造器的性能分析接口,并提供性能分析数据的详细报告。可以跨浏览器工作。

3.匿名函数

使用匿名函数或赋值函数可能会造成分析工具的数据变的混乱。分析匿名函数的最佳办法是给它们取个名字。使用指针指向对象的方法而不是使用闭包,可以实现最充分的分析覆盖率。

4.Firebug

1)控制台面板分析工具

Profile功能:

  • Calls表示函数被调用的次数。
  • OwnTime表示函数自身消耗的时间。
  • Time表示函数以及被它调用的函数所消耗的总时间。
2)Console API

Firebug提供了一个启动或停止性能分析的js接口:console.profile()和console.profileEnd()。

profileEnd会花费时间来生成报告,因此会增加脚本的开销。最好的方法是放在setTimeout中执行。

3)网络面板

这个面板以可视化的方式展示了脚本对其他资源的阻塞作用,可以深入探查脚本对其他文件加载造成的影响,以及对页面的一般影响。

第一条蓝色的垂直线,表示页面DOMContentLoaded事件触发的时刻,这个事件标志着页面的DOM树已经解析完成并准备就绪。

第二条红色的垂直线,表示window的load事件触发的时刻,意味着DOM准备就绪后所有外部资源也已加载完成。

5.IE开发人员工具

IE分析工具包括了函数分析,并提供一个包括调用次数、消耗时间以及其他性能数据的详细报告。该报告能以调用树的方式查看,还能分析原生函数,并导出分析数据。

6.Safari Web检查器

7.Chrome开发人员工具

Timeline面板提供了所有活动的概况,按分类可分为:Loading、Scripting、Rendering。

8.脚本阻塞

传统上,浏览器限制每次只能发出一个脚本请求,这样做是为了管理文件之间的依赖关系。只要一个文件依赖于在源码中靠后的另一个文件,那么它所依赖的那个文件必须保证在它运行之前准备就绪。脚本之间存在间隙就说明脚本被阻塞了。

新版浏览器解决这个问题的方法是允许并行下载,但阻塞运行,以确保依赖关系已经准备好。虽然这样能使文件下载得更快,但页面渲染仍然会被阻塞,直到所有脚本都被执行。

9.Page Speed

类似Firebug的网络面板,提供了Web页面加载的资源的信息。除了加载时间和http状态,他还能显示解析和运行js消耗的时间,指明可延迟加载的脚本,并报告那些没有被使用的函数。

10.Fiddler

一个http调试代理工具。

11.YSlow

YSlow工具可以深入观察页面初始加载和运行过程的整体性能。

12.dynaTrace Ajax Edition

是一个强大的Java/.NET性能诊断工具。提供了一个全面的性能分析,从网络和页面渲染到运行器脚本和CPU使用率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值