高性能JavaScript
一下内容是转载的,内容应该出自高性能JavaScript一书中,此书值得一读。
学习过程中写的笔记,有误请指正。
性能并不是唯一的考虑因素,在对性能要求并非苛刻的环境中,性能也可让位于:团队编码规范,个人编码习惯,代码可读性,模块可扩展性等因素。
以下提到的对性能的优化,仅仅提供了从性能的角度去阐释一些设计思路,但实际上,浏览器本身会逐步优化自身的性能问题,而我们那些提高性能的hack,可能会因为浏览器的版本更新,导致成为一种无用的hack,甚至让性能更慢,所以不要无谓的使用一些hack,去优化一些执行次数很少的代码,而降低代码的可读性,或增加代码量,,一句话:如非必要,请勿hack。
一 javascript加载和执行
1 无论是外链还是内联,script标签都会阻塞页面的渲染,所以script标签的位置最好是</body>前
2 减少http请求,合并多个script文件为一个,1个90k的文件比3个30k的文件载入速度要快
3 如何不阻塞页面,而载入一个脚本呢:
1)给script标签加defer属性:<script src=”xxx” defer></script>,这样不会使页面执行过程阻塞,但并不是所有浏览器都支持这个defer属性
2)xmlHttpRequest来请求脚本内容并eval,虽然这样可以灵活的控制脚本下载过程 及 何时执行,但不能跨域是个硬伤
3)createElement(‘script’)然后append是个不错的方案,无阻塞,有onload用onload,IE没有就用onreadystatechange实现加载完成的事件,注意readyState属性的值是complete或者loaded都要执行回调函数并清除onreadystatechange,因为这两个状态值不稳定。
4 所以推荐的方式是第三种:在载入js-loader部分的少量代码后,就用loader去加载其他js吧,注意一些市面上流行的库如labjs实现了这些功能(广告下:qwrap实现了依赖管理及动态加载 )
二 数据访问
数据存储在:直接量,变量,数组元素,对象成员中。
这又让我想起了编译型的语言,直接量存在于pe文件的.rdata段中,变量在调用栈上,数组元素和对象成员的访问需要基址+偏移量来定位,多层对象嵌套需要多次计算基址+偏移量来定位。
这些在javascript中依然没有太大变化:
1 直接量的访问无疑是迅速的
2 变量的访问需要考虑javascript允许函数进行嵌套定义,也就形成了基于函数定义的作用域链,而变量的访问,可能需要跨作用域来访问,所以这里有一点性能损失,但先进的js引擎用空间换时间(猜测在子作用域中缓存了所有父层作用域链上变量的name和地址,所以不会进行上溯作用域链,直接执行hash定位即可,但一个函数中如果包含eval,with,catch块的话,通过静态代码分析就没办法知道该函数中声明了哪些变量,也就无法做到这个优化),,不过,从性能上来看,与我的实际测试,大家编码的时候不需要注意这种性能考虑,按团队编码规范和个人编码习惯来吧。
3 对象成员的访问要考虑上溯原型链,所以理论上来说访问实例本身上的成员比访问原型的成员速度要快。
4 多层对象的嵌套要慢,但是在对性能要求并非很苛刻的环境中不用关心这些。
三 dom编程
1 dom的访问和修改
1) 标准dom方式(createElement,createTextNode,appendChild) 和 字符串拼接后设置innerHTML 之间的性能相差无几,各个浏览器不同
2) 节点克隆速度快一些,先用createElement创建好需要用到的元素类型后,以后在循环中调用元素的cloneNode来克隆
3) getElementsByName,getElementsByClassName,getElementsByTagName以及.images,.links,.forms属性返回的都是html集合,是个类数组,没有数组的方法,但提供了length属性和索引器,,HTML集合处于“实时状态”,底层文档对象更新时后自动更新javascript中的集合,访问length时候也会去查询,所以遍历集合时候要缓存length来提高效率
4) 遍历集合前记录length,循环体中避免对集合中某相同元素进行多次索引,一次索引到局部变量中,因为对html集合的索引性能很差,特别在某些老浏览器中
5) 遍历dom节点的话,综合来说使用nextSibling性能会比childNodes快一点,如果只遍历element的话,children被所有浏览器支持,所以尽量用children而不要用childNodes再自行筛选,其他的如childElementCount,firstElementChild,lastElementChild,nextElementSibling,previousElementSibling不被全面支持,注意做特性检测及兼容处理
6) 注意所使用的类库是否支持原生的querySelectorAll优先规则,querySelectorAll返回NodeList而不会返回HTML集合,不存在实时文档结构的性能问题
2 重绘和重排
改变dom节点的几何属性会引起重排(reflow),而后发生重绘(repaint)。
由于重排需要产生大量的计算,所以浏览器一般会通过队列化修改并批量执行来优化重排,获取最新布局信息的操作会导致强制触发队列的执行,如获取offsetTop,scrollTop,clientTop或调用getComputedStyle方法(currentStyle in IE)的时候,要返回诸如此类的最新的布局信息的时候,浏览器就会立即渲染队列中的变化,触发重排,然后返回正确的值。
由于动作的队列是基于页面的,所以,即使你获取的最新布局信息的节点没有待执行的动作,也会触发重排。
所以,尽量将修改元素样式的操作放在一起,然后再执行获取元素最新布局信息的操作,尽量不要交叉进行,因为每次获取元素的最新布局信息,都将触发重排和重绘操作。
虽然,现代浏览器进行了优化,并不会在每次设置元素的样式或改变dom的结构时都会重绘和重排,,但旧版浏览器仍会有性能问题,所以尽量用以下规则来最小化重绘和重排:
1) 设置样式:使用cssText属性来合并更新的样式信息: el.style.cssText=”padding-left:10px;border:1px”
2) 改变dom结构:将元素脱离文档流,然后进行一系列改变,然后再带回文档流中,方法如下:
(1) 隐藏元素,对元素的dom结构进行一系列更改,再显示元素
(2) 使用createDocumentFragment创建文档碎片,针对文档碎片进行批量操作,然后一次性添加到文档中
(3) 将原始元素clone到一个脱离文档流的节点中,修改这个副本后,替换原始元素
3) 缓存布局信息
尽量减少布局信息的获取次数,如果有针对布局信息的迭代操作,先将布局信息保存到局部变量中,对该局部变量进行迭代更新,然后将该局部变量更新到dom上
4) 动画效果时脱离文档流
一个元素进行动画效果时:
(1) 将该元素脱离文档流,比如绝对定位该元素
(2) 对该元素进行动画操作,这样不会触发其他区域的重排和重绘
(3) 动画结束时,将元素恢复文档流位置,这样只会对其余区域进行一次重排和重绘
3 使用事件委托来减少dom树上的事件响应函数
对文档中大量的元素进行事件绑定会导致运行时效率下降,基于事件都会冒泡到父层,可以在父层上绑定一个事件,然后识别target(srcElement)来自行dispatch事件。
四 算法和流程控制
1 循环
1) while,for,do-while性能上基本没差别,不过while和do-while一般被用于基于某个条件的循环,而for用于数组的迭代或线性的工作,而for-in用于对象的枚举
2)减少循环次数或减少循环中的工作量都可以优化性能,如duff循环,但性能提升微乎其微,实际测试,在某些浏览器下duff循环不仅不会提升性能,还会降低性能,另外倒序循环可能会快一些,毕竟正序是与长度进行对比后的boolean值,而倒序循环是将表示当前循环进度的数值转换为boolean
3) 迭代器如js1.6的forEach方法等,性能比用for进行循环要慢一些,但更语义化
附(我写的一个duff循环的js版本)
- //注:duff的原理是减少循环次数,从而减少对循环条件的判断,所以duff的性能优化只对超大数组(10万次条件判断会降低为10万/8次条件判断)有意义,与循环体中的语句数量无关,所以,duff只适用于循环体执行速度非常快,而循环规模非常大的状况,在js中只有略微的性能提升
- function duff(list,callback){
- var i = list.length % 8;
- var tails = i;
- while(i){
- callback(list[--i]);
- }
- var greatest_factor = list.length-1;
- do{
- process(list[greatest_factor]);
- process(list[greatest_factor-1]);
- process(list[greatest_factor-2]);
- process(list[greatest_factor-3]);
- process(list[greatest_factor-4]);
- process(list[greatest_factor-5]);
- process(list[greatest_factor-6]);
- process(list[greatest_factor-7]);
- greatest_factor-=8;
- }while(greatest_factor>tails);
- }
2 条件语句
1)switch比if-else快一些,但性能微乎其微,建议在数量较多的分支时使用switch,而进行范围判断或多重条件的时候使用if-else
2)if-else的排列从大概率向小概率
3)如果条件太多,建议使用查找表,而且查找表具有动态扩充的能力
3 递归
1) 由于调用栈的限制,递归是很危险的
2) 非自调用,而是交叉调用形成的“隐伏递归”是很危险的,出错之后很难排错
3) 尽量将递归转化为迭代,比如树的遍历,用非递归的dfs,bfs来实现就好
4) 很常用的函数,进行memoize,用空间换取时间
五 字符串和正则表达式
1 理解各个浏览器的js引擎字符串合并的内部机制:
1) 避免产生临时字符串,如用str+=’abc’;str+=’def’ 而不要用 str+=’abc’+'def’
2) firefox会在编译期合并字符串常量,如str+= ‘abc’+'def’会被转化为str+=’abcdef’,yur-compressor也有这个功能
3) 数组的join方法,性能不会比+连接符更快,因为大多数浏览器的+连接符不会开辟新内存空间,而ie7,ie6却会开辟新空间,所以ie6,7中字符串连接应该用数组的join,而其他浏览器用+连接符,,concat方法是最慢的方式
2 正则表达式优化
基于各js引擎中正则表达式引擎的不同,以下某些方法会带来某引擎性能的提升但可能同样导致其他引擎性能的下降,所以原书原文也只是原理性阐述,实际开发时视具体情况而定。
1) 循环中使用的正则对象尽量在循环前初始化,并赋予一个变量
2) 编写正则表达式时,尽量考虑较少的回溯,比如编写分支时将大概率分支放在前面,在贪婪模式是从尾部向前回溯,懒惰模式从headPart向尾部逐字符回溯
3) 关于回溯失控,起因是太宽泛的匹配模式,如最后的part没能成功匹配,则会记住回溯位置,尝试修改前面的part的匹配方式,如尝试让前面的懒惰模式包含一次endPart,然后从新位置再尝试匹配,正则引擎会一直尝试,最终导致回溯失控
4) 在只是搜索确定的字面量,及字面量位置也确定如行首,行尾等,正则非最佳工具
(详细的正则优化先略过,等以后再回来写吧)
六 快速响应用户界面
1 浏览器UI线程和UI队列
UIThread:javascript和ui是共用同一个线程的,这样做的好处是无需考虑运行时的用户态或核心态的线程同步,也无需在语言层面实现临界区(critical section),在我最初自己摸索javascript的时候,认为javascript也是多线程的,后来写代码测试后发现,javascript并非多线程,且javascript代码的执行会阻塞和ui共用的这唯一的线程,使用户的操作得不到ui的反馈。
UIQueue:上面的UIThread负责执行UIQueue中的task,无论用户对UI采取的动作触发的dom事件响应函数,还是javascript执行过程中用setTimeout或setInterval创建的定时器事件响应函数,都会被插入到UIQueue中,当UIThread处于busy状态时,可能会忽略掉一些task,不置入UIQueue,比如用户动作的产生的ui更新task和触发的dom-event两者,ui更新的task会被忽略不放入UIQueue,而dom-event会放入UIQueue等待UIThread处于idle状态时执行,而setInterval函数的周期性定时器事件,会视UIQueue中是否有相同的事件响应函数,如没有才会将该task置入UIQueue。
UIQueue的task来源:
1) 用户操作产生的ui更新重绘
2) 用户操作触发的绑定在dom上的javascript事件
3) dom节点自身状态改变触发的绑定在自身上的javascript事件,如的onload
4) setTimeout与setInterval设置的定时器事件
5) ajax过程中的onreadystatechange事件,这个javascript函数并非绑定在dom上,而是绑定在xmlHttpRequest对象上
浏览器限制:为了避免某个javascript事件函数执行时间过长,一直占据UIThread,从而导致用户操作触发的UIUpdate任务得不到执行,各个浏览器使用不同的方案限制了单个javascript事件函数的执行时间
1) ie:500万条,在注册表有设置
2) firefox: 10秒,在浏览器配置设置中(about:config->dom.max_script_run_time)
3) safari: 5秒,无法修改
4) chrome: 依赖通用崩溃检测系统
5) opera: 无
界面多久无反应会让用户无法忍受: 最长100毫秒,用户的动作之后超过100毫秒界面没有做出响应,用户就会认为自己和界面失去了联系。
2 用定时器让出时间片断(分解任务)
任务分解过程:用户无法忍受一个10秒的任务占据UI线程,而自己的任何操作得不到反馈,于是我们可以将这个10秒的任务分为200次50毫秒的任务,每执行50毫秒,让出UI线程去执行UI队列中的界面更新的任务,让用户及时得到反馈,然后再执行50毫秒,直到执行完毕。
基于大多数长时间UI任务都是对数组的循环操作,于是我们可以将这个循环过程进行拆解,示例代码:
- function timedProcessArray(list, callback, complete, progress){
- var total = list.length;
- var curProgress = 0;
- var preProgress = 0;
- (function(list, iteration){
- var fn = arguments.callee;
- var st = +new Date();
- while(list.length && (+new Date() - st < 50) ){
- iteration = callback(list.shift(), iteration);
- }
- if(list.length){
- if(progress){ //如果需要对进度进行通知)
- curProgress = 100 - (100 / total * list.length ^ 0);
- if(curProgress != preProgress){
- preProgress = curProgress;
- progress(curProgress);
- }
- }
- setTimeout(function(){
- fn.call(null, list, iteration);
- }, 25);
- }else{
- progress && progress(100);
- complete && complete(iteration);
- }
- })(list.concat(),0);
- }
阻塞和非阻塞(任务分割)方式的示例,:http://lichaosoft.net/case/progress.html
3 web-workers
在web-workers之前,javascript是没有多线程的,web-workers标准带来了真正的多线程,web-workers本来是html5的一部分,现在已经分离出去成为独立的规范:http://www.w3.org/TR/workers/
和web-workers之间仅能通过onmessage和postMessage交互。
使用web-workers以辅助线程进行计算并拥有进度通知的示例:
http://lichaosoft.net/case/worker.html (请使用支持web-workers的chrome或safari浏览)
web-workers适用于那些无法拆解的任务,对数组的遍历是一个可以被拆解的任务,对树的遍历通过使用dfs或bfs将树平坦化为数组后也可以进行拆解,不能拆解的任务:
1) 编码/解码大字符串
2) 复杂数学运算
3) 大数组排序
超过100毫秒的任务,如浏览器支持web-workers,优先使用web-workers,如不支持则使用timedProcessArray进行分割运行。
没有任何javascript代码的重要度高于用户体验,用户体验是至高重要的,无论如何不能让用户觉得界面反应速度慢。
七 AJAX
从广义上来看,AJAX是指不重载整个页面的情况下,与服务端进行数据传输,解析数据,并局部刷新页面区域的改善用户体验的行为,那么我们下面介绍:数据传输,数据格式。
1 数据传输
1) XHR: 创建一个XMLHttpRequest对象与服务端通信
2) 动态脚本注入: 这是一个hack,创建一个script元素并设置src为任意uri-A(可跨域),可在页面中先定义一个数据处理函数如function newsListProc(list){},然后在该uri-A指向的script文件中调用newsListProc并将数据传入,这项技术也称为:JSON-P。
3) mXHR: 将多个资源文件使用XHR传输到浏览器端,js负责对数据流分割,然后dispath给不同类型资源文件的处理函数
4) 流式XHR: 在支持readyState为3时,可访问已解析好的部分xhr数据,既是支持流式XHR,可进行流式处理来优化执行效率,目前实际测试ff3.6支持,而ie6,7,8及chrome都不支持流式XHR。
5) iframes: 待续
6) comet: 待续
注1:关于mXHR和流式XHR,我写了一个demo用来演示多资源文件合并,由XHR向客户端传输,并在支持流式XHR的浏览器中使用流式XHR,DEMO地址:
注2:纯粹的发送数据而无需接受数据,可用beacons方式,类似动态脚本注入,不过创建的不是script元素,而是Image对象,并设置src为要请求的URI,所以这种方式只能使用GET方式,代码示例:
- function keepalive(uri, delay){
- var beacon, delay = delay || 1000,
- timer = setTimeout(function(){
- var fn = arguments.callee;
- beacon = new Image();
- beacon.onload = beacon.onerror = function(){
- timer = setTimeout(fn, delay);
- }
- beacon.src = uri;
- }, delay);
- return function(){
- console.log('stop');
- clearTimeout(timer);
- };
- }
2 数据格式
1) XML: 使用responseXML对象的getElementsByTagName,getElementById,node.getAttribute等api对xml文档进行解析,也可以用XPath进行解析,性能更好些,硬伤是:
(1) “结构/数据”比 太高
(2) XPath在支持并不广泛
(3) 最重要的就是需要先知道内容的详细结构,针对每个数据结构编写特定的解析方法
2) JSON: 最广泛的数据格式,解析性能较之xml高,”结构/数据”比低,如使用缩略属性名或完全使用多层数组格式,结构数据比更低,传输性能更高
3) JSON-P: 无需解析,属于javascript的正常函数调用,性能最高
4) HTML: 无需解析,服务端已构造好用于局部更新的html数据,直接用innerHTML更新,数据结构比太高,压力被集中在服务端,网络传输数据量高
5) 自定义分隔符: 性能最高,结构数据比最低,对于性能要求比较苛刻的环境中使用
附:创建xhr对象的代码:
- function createXhrObject(){
- if(window.XMLHttpRequest){
- return new XMLHttpRequest();
- }else{
- var msxml_progid = [
- 'MSXML2.XMLHTTP.6.0', //支持readyState的3状态,但此状态时读取responseText为空,觉得似乎没意义。。
- 'MSXML3.XMLHTTP',
- 'Microsoft.XMLHTTP',
- 'MSXML2.XMLHTTP.3.0'
- ];
- var req;
- for(var i=0;i<msxml_progid.length;i++){
- try{
- req = new ActiveXObject(msxml_progid[i]);
- break;
- }catch(ex){}
- }
- return req;
- }
- }
从字符流中异步解析数据的工具类
- /*
- * @method StringStreamParser 流式字符串异步解析类
- * @param onRow 解析出数据行的回调函数
- * @param RowSeperator 行分隔符,默认'u0001'
- * @param ColSeperator 列分隔符,默认'u0002'
- */
- function StringStreamParser(onRow, rowSeperator, colSeperator){
- if (!(this instanceof arguments.callee)){
- return new arguments.callee(onRow, rowSeperator, colSeperator);
- }
- var stream = '';
- rowSeperator = rowSeperator || 'u0001';
- colSeperator = colSeperator || 'u0002';
- /* @method write 向字符流写入包
- * @param packet 写入的包
- * @param lastPacket 是否为最后一个包,默认false
- */
- this.write = function(packet, lastPacket){
- stream += packet;
- var rowIdx = stream.indexOf(rowSeperator);
- var colIdx,strRow, dataRow;
- while(rowIdx!==-1 || lastPacket){
- if(rowIdx===-1){
- strRow = stream.substr(0);
- lastPacket = false;
- }else{
- strRow = stream.substr(0,rowIdx);
- stream = stream.substr(rowIdx+1);
- rowIdx = stream.indexOf(rowSeperator);
- }
- dataRow = [];
- while(colIdx = strRow.indexOf(colSeperator)){
- if(colIdx !== -1){
- dataRow.push(strRow.substr(0, colIdx));
- strRow = strRow.substr(colIdx+1);
- }else{
- dataRow.push(strRow.substr(0));
- break;
- }
- }
- onRow.call(null, dataRow);
- }
- }
- }
3 其他
在确定了合适的数据传输技术,和数据传输格式之后,还可以采取以下方式酌情优化ajax:
1) 缓存数据: 最快的请求,是不请求,有以下两种方式缓存:
(1) 对于GET请求,在response中,设置expires头信息,浏览器即会将此请求缓存,这种缓存是跨会话也是跨页面的
(2) 在javascript中,以url作为唯一标识符缓存请求到的数据,无法跨页面也无法跨会话,但可编程控制缓存过程(不能跨会话也不能跨页面,这种缓存基本上是无意义的)
2) 在必要的时候,直接使用XHR对象而非ajax库,如需要流式的数据处理
八 常见编码中的性能提高点
1 避免双重求值: eval,Function,setTimeout和setInterval都允许传入字符串,而此时会创建一个新的编译器的实例,将会导致很大的性能损失。(另外这些方式执行代码时,代码的作用域在各个浏览器也不尽相同,容易掉坑)
2 使用Object/Array的直接量进行定义(且直接量比new Object()然后设置属性更节省代码)
3 不要让代码重复运行
1) 延迟定义
2) 条件预定义
4 尽量使用原生javascript