第一章、加载和执行

1.1、脚本的位置

由于javascript脚本会阻塞页面其他资源的下载,因此推荐将所有的script标签尽可能放到body标签的底部,以尽量减少对整个页面下载的影响

1.2、合并脚本

尽量减少页面中外链脚本文件的数量,也会改善性能,因此构建器才会最终把js文件都打包成一个文件(下载单个100kb文件将比下载4个25kb文件更快)

1.3、无阻塞脚本

1.3.1、延迟脚本

HTML4中为script标签定义了一个扩展属性defer,该属性表明本脚本不会修改DOM,因为此代码可以安全的延迟执行;

<script type='text/javascript' src='file1.js' defer></script>

带有defer属性的脚本可以放在任何地方,对应的js文件将在页面解析到script标签时开始下载,但并不会执行,直到DOM加载完成(onload事件前),这种文件下载不会阻塞浏览器其他进程,可以与页面中其他资源并行下载

1.3.2、动态创建脚本

用DOM方法动态创建一个新的script元素

var script = document.createElement('script')
script.type= 'text/javascript'
script.src = 'file1.js'
document.getElementByTagName('head')[0].appendChild(script)

这个新创建的script元素加载了file.js文件,文件会在该元素被添加到页面时开始下载,这种技术重点在于:无论何时启动下载,文件的下载和执行都不会阻塞页面的

注意:当代码只包含工业面其他脚本调用的接口时,就会有问题,你必须跟踪并确保脚本下载完成且准备就绪,在Firefox、Opera、Chrome、Safari3以上版本通过onload事件来实现,但在IE中支持另一种方式,它会触发readystatechange事件,script元素提供一个readyState属性,共5种状态:uninutialized(初始状态)、loading(开始下载)、loaded(下载完成)、interactive(数据完成下载但还不可用)、complete(所有数据准备就绪),上述的loaded和complete两个状态要兼容判断,if(script.readyState===‘loaded’ || script.readyState===‘complete’)

1.3.3、XMLHttpRequest脚本注入

使用XMLHttpRequest对象获取脚本并注入页面中

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

这种方式的优点:所有主流浏览器都能正常工作
局限性:js文件必须与所请求的页面处于相同的域

第二章、数据存取

数据的存取位置会很大程度上影响其读写速度,javascript中有下面4种基本的数据存取位置:字面量本地变量(开发用var定义的)、数组元素对象元素,每一种数据存取的位置都有不同的读写消耗,一般情况下字面量和局部变量中的差异微不足道,访问数组和对象的代价则高一些,至于多少,不同浏览器会有不同

既然这里说数组和对象对性能影响更大,我们就来看看为什么会这样?

  • 在执行环境的作用域链中,一个标识符所在的位置越深,则它的读写速度也就越慢,在函数中局部变量的速度总是最快的,因为局部变量存在于作用域链的起始位置,全局变量总是处在作用域链的最末端,因此访问速度也是最慢
  • 如果某个跨作用域的值在函数中被引用一次以上,那么最好把它存储到局部变量中
  • 尽量避免使用with和try…catch的catch语句,因为这两种语句会新创建一个执行环境,改变当前作用域链,它会将新创建的执行环境推入作用域的顶部,所以就加长整个作用域链了,会影响性能
  • 闭包代码执行时也会创建一个执行环境,也会新创建一个执行环境,改变整个作用域链,带来性能损失
  • 解析对象成员和上面第一条解析变量是类似的,如果使用一个不属于自身的属性时,引擎会在该对象的原型链中去寻找,位置越深就会越慢
  • 对象成员嵌套的越深,读取速度就会越慢(就是使用对象的.标识符),例如window.location总比window.location.href快

第三章、DOM编程

访问和操作DOM是web应用重要的一部分,你可以把javascript和DOM想象成两个岛屿,每次操作和访问就相当于在两岛之间经过,每次否要收取过桥费(性能损失),所以为了减少DOM编程带来的性能损失,有以下几点注意:

  • 最小化DOM的访问次数,尽可能在javascript端处理
  • 一般节点克隆比新创建一个元素节点会更有效率
  • 像类似document.getElementsByTagName()这种dom方法会返回一个HTML集合,读取这样一个集合的length要比读取一个正常数组慢很多,所以一般涉及这种操作时,可以将一个HTML集合拷贝到普通数组中
function toArray(coll){   //传入HTML集合
	var len=coll.length;
	a=[];
	for(var i = 0;i<len;i++){
		a[i]=coll[i]
	}
	return a
}
  • 当遍历访问HTML集合中元素时,第一原则是把HTML集合存储在局部变量中,第二是把length缓存在循环外部,第三使用局部变量替代这些需要多次读取的元素
  • 在某些情况下,只需要访问元素节点时,要在循环中检查节点类型并把不属于元素节点的过滤掉,如果这些操作在js中进行的话效率不高,最好使用DOM中的这些属性,它们能区分元素节点(HTML标签)

推荐使用的属性名

被替代的属性名

children

childNodes

childElementCount

childNodes.length

firstElementChild

firstChild

lastElementChild

lastChild

nextElementSibling

nextSibling

previousElementSibling

previousSibling

  • 当需要对特定DOM元素操作时,可以使用document.querySelectorAll来替代原先的document.getElementButagName(),效率更高,使用也更灵活,具体用法可以自行查询,但是注意,最好用之前判断下浏览器是否支持
  • 要留意重排和重绘,尽量最小化重排和重绘,应该合并多次对DOM和样式的修改,一次处理掉(例如可以使用cssText来处理多个样式的一同修改)
  • 批量修改样式时,为了较少重绘和重排次数,可以使元素暂时脱离文档流(隐藏元素,修改完后重新显示、使用文档片段或者拷贝到一个脱离文档的节点中,操作完后再替换掉原始元素)、“离线”操作DOM树或者使用缓存的方式
  • 当元素很多时,要尽量避免使用:hover这种效果,会很影响性能的
  • 当页面中存在大量元素,而且每一个多要绑定不同事件时,不要一个个分别绑定事件,这样很影响性能,可以使用事件委托,在其父元素中绑定一个事件就行,通过不同的触发源来区分处理事件;

第四章、算法和流程控制

  • js中的四种循环,其中for 、while、do-while性能相当,就是for-in循环性能相对于前面的要差一点,所以尽量不要使用for-in循环,除非你需要遍历一个属性数量未知的对象
  • 改善循环性能最佳的方式是减少每次迭代的运算量(例如可以把循环长度用一个变量先单独拎出来,不要放到循环中)和减少循环迭代次数
  • 基于函数的迭代(foeEach)比循环的性能要稍微差点
  • js的条件语句中,switch比if-else快,当然两者的使用要看情况,如果判断条件少的时候(1-2)推荐使用if-else,大于2个以上推荐使用switch
  • 当判断条件较多时,使用查找表(就是将不同的返回值都放在对象或数组中,把判断字段当成key去匹配,返回不同的值)比if-else和switch都快
  • 如果你要优化if-else的话,最简单的方法就是将最可能出现的条件放到首位判断,这样可以减少判断次数
  • 浏览器的调用栈大小限制了地递归算法在js中的应用,栈溢出错误会导致其他代码中断运行
  • 如果你遇到栈溢出错误,可将方法改为迭代算法,或使用Memoziation来避免重复计算

第五章、字符串和正则表达式

  • 当连接数量巨大或尺寸巨大的字符串时,数组项合并是唯一再IE7及更早版本中性能稳定的方法
  • 如果不考虑IE7及更早版本的性能,数组项合并方法是最慢的字符串链接方法之一,推荐使用+或者+=,来避免不必要的中间字符串
  • 回溯既是正则表达式匹配功能的基本组成部分,也是影响正则表达式低效的源头
  • 正则表达式并不总是完成工作的最佳工具,尤其是你只搜索字面字符串时

第六章、快速响应的用户界面

浏览器UI线程

用于执行js代码和更新用户界面的进程通常被称为“浏览器UI线程”,UI线程工作基于一个简单的队列系统,任务会被保存到队列中,直至进程空闲时,队列中的下一个任务就会被提取出来并运行;

举个例子分析UI进程的整个运行过程:用户点击一个按钮,按钮触发一个点击方法,方法中创建一个新的div元素,往里面填充已点击字样

上述操作的整体过程:当按钮被点击时,它会触发UI线程来创建两个任务并添加到队列中,一个是按钮被点击后的状态改变,执行完后还有一个是点击的js代码运行,在执行js代码这个任务时,又创建了一个新的div元素,这又会创建出一个新的UI更新任务,加入到队列中,所以当js执行完后又会执行这个新添加的UI更新任务,简单概括就是如下几步:

  1. 创建两个任务,一个按钮点击的UI更新任务,还有一个js执行任务
  2. 先执行按钮被点击的UI更新任务
  3. 再执行js运行任务
  4. js执行过程中又创建一个UI更新(含有内容的div被创建)被加入到队列中
  5. js执行任务结束后执行新添加的UI更新任务;
  6. 所有任务执行完毕后,UI线程处于“空闲”状态;等待新的任务产生和执行

注意:再大多数浏览器中,当在执行js任务时会禁止加入新的UI更新任务,所以js任务必须尽快结束,不然对用户的体验造成不良影响

浏览器限制:

有时浏览器会限制js任务的执行时间,这种限制是有必要的,为了防止被恶意代码控制,限制分为两种:调用栈大小限制以及长时间运行脚本限制

不同的浏览器对检测长时间运行脚本的方式不同:

  • IE自第四版以后,默认限制500万条语句;
  • Firefox默认限制为10s
  • Safari默认限制5s,该限制无法修改
  • Chrome没有单独的长运行脚本限制,但它依赖其通用奔溃检测系统来处理该问题
  • Opera没有长运行脚本限制,它会继续执行js代码,直至结束

多久才算久?

虽然上面说到了浏览器的限制时间挺久的,但是你的js代码绝不允许有这么长的运行时间,官方表示最好限制所有js任务在100毫秒或更短时间内完成

如何解决UI任务无法正常执行问题(js执行时间过长)?

使用定时器让出UI线程执行权(即停止当前js任务,让其他UI任务先执行)

使用定时器处理数组

有时在遍历一个长度过长的数组时,或者单次循环体内的操作过于复杂时,整个循环的过程和时间就会很长,有什么办法能解决这类数组耗时久的问题呢?————定时器
当然要使用定时器来解决这类问题的,该数组还必须满足以下两个条件:

  1. 处理过程不需要同步
  2. 数据不一定要按照顺序处理

如果该数组循环同时满足以上两个条件时,就可以用定时器来分解任务;

var todo = items.concat();  //克隆原数组
setTimeOut(function(){
	process(todo.shift());  // 取得数组的下一个元素并进行处理
	if(todo.length>0){   // 判断是否还有别的任务,再创建一个定时器
		setTimeOut(arguments.callee,25)
	}else{
		callback(items)
	}
})

Web Workers

H5新增的一种方法,可以创建新的worker线程来处理那些耗时久的代码,这里该内容就不做具体介绍了,可以自行上网查询

第七章、Ajax

传输技术方面

  • XMLHttpRequest(XHR):是目前最常用的技术,不支持跨域请求,而且低版本不支持”流“,也不会提供readyState为3的状态,使用GET请求的数据会被浏览器缓存起来(适用于URL长度不长的情况下),如果请求数据多还是建议用POST请求(因为IE浏览器对URL长度限制)
  • 动态脚本注入:它能跨域请求数据,但它能提供的控制有限,比如说你不能设置请求头信息,参数传递只能使用GET方式,不能设置请求超时或者重试等等
  • MXHR:允许客户端只用一个HTTP请求就可以从服务端获取多个资源,通常是服务端将资源(css、html、js或图片)打包成一个由双方约定的字符串分割的长字符串并发送到客户端,然后用js代码处理这个长字符串,并根据它的mime-type类型和传入的其他”头信息“解析出每个资源,由于该方式响应的数据体积较大,因此有必要在每个资源收到时就立即处理,而不是等所有资源都完成,所以要监听readystate为3的状态,这种方式最大的缺点就是获得的资源不能被浏览器缓存,以及对于IE的老版本不支持readyState,这种方式对于页面中包含大量其他地方不用的资源(图片),因此不需要缓存的情况下非常适用,减少了大量的HTTP请求次数

数据结构方面

  • XML:被广泛应用而且支持良好,但它十分笨重且解析慢,不推荐使用
  • JSON:是轻量级的,解析速度快,通用性和XML相当(推荐
  • HTML:只适用于特定的场合,但它可以节省客户端的CPU周期
  • 自定义数据结构:字符分隔,它非常轻量,然后使用split方法进行解析处理,该方法在解析大量数据时速度非常快,但需要编写额外的服务端构造程序,并在客户端解析

其他优化准则

  • 减少请求数,可通过合并js和css文件,或使用MXHR;
  • 缩短页面的加载时间,页面主要内容加载完成后,用Ajax获取那些次要的文件
  • 确保你的代码错误不会输出给用户,并在服务端处理错误
  • 知道何时使用成熟的Ajax类库,以及何时编写自己的底层Ajax代码

第八章、编程实践

  • 通过避免使用eval和Function()构造器来避免双重求值带来的性能消耗,同样的,给setTimeOut()和setInterval()传递函数而不是字符串作为参数;
  • 尽量使用直接量来创建对象和数组,不要使用new 构造函数的形式;
  • 避免做重复的工作,当需要检测浏览器的一些兼容时,可使用延迟加载和条件预加载的方式来优化;
//这里讲下条件预加载的方式
原先定义方式:这种方式就每次调用addHandle方法都会进行兼容判断
function addHandle(target,type,handle){
	if(target.addEventLinstener){  
		target.addEventLinstener(type,handle,false)
	}else{   //IE支持
		target.attachEvent('on'+type,handle)
	}
}

预加载方式:提前在函数定义时就做了一次判断,后续每次调用时就不会		再判断了
const addHandle = document.body.addEventLinstener ? 
function(target,type,handle){
	target.addEventLinstener(type,handle,false)
}:
function(target,type,handle){
	target.attachEvent('on'+type,handle)
}
  • 当判断奇偶情况做不通处理时,可以用位运算符& (“数” & 1)来替换原先的( “数” %2),使用位操作符会更快些;
  • 在进行数学计算或者DOM操作的时候,尽量先使用原生的方法;