用一周的时间看完了《高性能Javascript》,涉及到了javascript很多个方面的性能问题,但由于书比较薄,基本点到即止,但还是刷新了我之前的一些对javascript的误解甚至误用,还让我了解到了javscript的一些冷知识和独到的技巧。读书过程对其中一部分做了笔记,但没有全部,有些没有在电脑前看就没有记录下来了。
JS 从循环性能到 Duff's Device
JS的循环种类和性能:
1、标准for循环
for (var i = 0, len = list.length; i < len; i++) {
// todo...
}
2、while循环
又分前侧循环和后测循环:
while(i--) {
// todo ...
}
do {
// todo ...
} while(i--)
3、for-in 循环
for (var i in obj) {
// todo
}
前两种循环的性能基本一样,唯独第三种循环的性能明显比前两种低,问题在于每次迭代操作都会搜索实例或者实例属性, 速度大约为其他两种的1/7,除非你需要迭代属性未知的对象,否则尽量不要使用这种方法去迭代。
宁可罗列出该对象的属性,然后用for循环去遍历该对象:
var props = ['props1', 'props2']
, i
;
for (i = 0; i < props.length; i++) {
process(obj[props[i]]);
}
减少迭代中的操作次数
每次迭代,减少对遍历对象的属性读取如长度,对于重复的存为局部变量,减少初始化的变量,这些应该很好理解:
var i = arr.length;
for (; i > 0;i--) {
// todo
|
** 再深一步:减少迭代的次数 **
Duff's Device 是一种循环体展开技术,它让每一次迭代执行了多次迭代的操作,从而减少了迭代的次数。
var iterations = Math.floor(items.length / 8)
, start_at = item.length % 8
, i = 0
;
do {
switch(start_at ) {
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++]);
}
start_at = 0;
} while(iterations--);
如果迭代次数超过1000,执行效率会提高70%左右,笔者试验了结果,发现无论是否是使用了这种技术,效率上并没有所谓的提升,Demo
** 优化这种技术 **
var leftover = items.length % 8
, i = 0
;
while(leftover--) {
process(items[i++]);
}
var iterations = Math.floor(items.length / 8);
while(iterations--) {
process(items[i++]);
}
** ECMA第四版提供了一个新的原生数组方法forEach(), **
尽管该方法能很便利地去遍历每个成员,但是它仍然比基于循环的迭代要慢一些,对每一数组项调用外部方法所带来的开销是速度慢的主要原因
递归与调用栈
** 调用栈的限制引出安全问题 **
Javascript引擎支持的递归数量与Javascript调用栈大小直接相关, 只有IE例外,它的调用栈和系统空闲内存有关,其他浏览器都有固定数量的调用栈闲置。
递归可能是自身调用的递归,可能是相互的递归,如A调用B, B调用A,这种情况如果出错很难找到问题,为了安全,尽可能地使用迭代的方式。
** 迭代来解决安全问题 **
使用优化后的迭代替代长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少得多。
** 使用内存缓存来减少递归的调用 **
function menfactorial(n) {
if (!menfactorial.cache) {
menfactorial.cache = {
'0': 1,
'1': 1
}
}
if (!menfactorial.cache.hasOwnProperty(n)) {
menfactorial.cache[n] = n * menfactorial(n-1)
}
return menfactorial.cache[n];
}
字符串和正则表达式
** 使用数组连接字符串更快?**
这句话对又不对,之所以会出现使用数组连接字符串,是由于傻逼的IE7的连接算法要求浏览器在循环过程中为逐渐增大的字符串他不断复制并分配内存,结果是运行时间和内存消耗以平方关系递增,于是当我们使用数组去连接时,由于一次分配够了内存,性能就得到了很大的提升。
然而对于IE8后和其他浏览器,数组连接和普通的字符串连接性能差异其实不大,详情可以看DEMO
** concat **
String.prototype.concat 又怎么样呢?比使用简单的+和+=稍慢,比join也慢,所以不建议使用。
** 慎用正则表达式 **
从编译、设置起始位置、匹配每个正则表达式到匹配成功与失败
正则中的回溯是影响其速度的重要部分
trim的实现:
if (!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+/, '').replace(/\s+$/, '');
}
}
快速响应的用户界面
** 100毫秒准则 **
如果界面在超过100毫秒响应用户的输入,那么用户会感觉自己与界面失去了联系,由于js运行时无法更新UI,所以如果javascript的运行超过100毫秒,用户会感觉失去了对界面的控制
除了脚本运行的时间,如果网站采用了异步脚本加载的技术,那么还要考虑脚本加载过来的时间,这个时候采用预加载的方式可以减少由于脚本加载而带来的延迟,比如但用户点击某个按钮触发脚本加载时,我们可以改为但用户把鼠标放在按钮上时就去加载脚本,这样,等到用户去点击的时候,脚本就已经加载好了。
** 定时器 **
当脚本运行时间超过100毫秒,需要想办法分割执行代码段,从而让界面能够响应用户的交互:
function processArray(items, process, callback) {
var todo = items.concat();
setTimeout(function() {
process(todo.shift());
if (todo..length > 0) {
setTimeout(arguments.callee, 25);8
} else {
callback(items);
}
}, 25);
}
使用定时器对循环进行分割是一种方法,请看demo,同理,你也可以对要执行的函数队列进行分割执行,同时记录每个函数的运行时间。
注意一个页面最好只有一个定时器,一方面我们可以更好地控制时间队列,防止多个定时器抢占时间队列,另一方面防止过多的时间器导致时间分割过小反而引起性能的问题。
** Web Worker **
新版浏览器支持的特性,允许在UI线程外执行javascript代码,从而避免锁定UI
Ajax
五中常用技术用于向服务器请求数据
- XML
- Dynamic script tag insertion
- iframes
- Comet
- Multipart XHR
编程过程的性能消耗
** 昂贵的双重求值 **
js和其他脚本语言一样,允许我们在程序中提取一个包含代码的字符串,然后动态执行它,可以通过eval、function构造函数和两个定时器
var num1 = 1, num2 = 2
;
var sum = eval('num1 + num2');
var sum2 = new Function('arg1', 'arg2', 'return arg1 + arg2');
setTimeout('sum = num1 + sum2', 1000);
setInterval('sum = num1 + sum2', 1000);
字符串代码会以正常方式求值,然后再对里面的代码进行求值,代价昂贵。
** 直接使用Object/ Array直接量 **
尽管我们可以使用像其他语言的方法一样先实例化构造函数,然后再赋值:
var obj = new Object();
obj.name = 'cjs';
obj.count = 30;
当使用直接量更加快,不仅代码量小了,运行更加快
var obj = {
name: 'cjs',
count: 30
}
** 使用延迟加载和预加载 **
前端经常要兼容各个浏览器,导致了对于一个功能的函数,往往需要判断用户所在的浏览器再执行不同的功能,我们可以使用延迟加载或者预加载的方式来防止每次执行时的判断
** 位运算 **
基于底层操作的位运算是javascript运行最快的部分之一,使用的好可以提升一倍以上的速度,这是为什么很多图形处理的代码都使用位运算符进行计算的原因之一。
使用与运算符 & 可以用来替代判断奇数和偶数,使用或运算符可以用来组合数字,还有按位左移(<< )和按位右移(>>)和无符号右移(<<)等运算符
** 原生方法 **
javascript的原生部分都是用低级语言写的,如C++, 意味着这些方法已经被编译为机器码,成为浏览器的一部分,所以永远比我们自己写的代码要快。
如Math对象提供的方法、css选择器API等,使用原生的querySelector查询所用的时间是jq查询的10%
### 构建部署高性能的javascript应用
前端构建工具提供了代码文件的合并、压缩,HTML5的离线应用缓存等概念