# 高性能网站建设进阶指南第一部分
# Javascript性能
# 理解Ajax性能(性能优化前做权衡)
项目三角形
指短时间、高质量和低成本。往往只能满足其中任意两点
算法选择运行时间快、占用内存小,抢占了市场,牺牲了代码高质量
权衡影响开发方式
增量式开发:采用任务调度和分阶段策略,系统各部分以不同时间和速度进行开发
大爆炸集成:直接开发整个系统
关注程序性能时,需斟酌权衡因素如改善代码质量、避免产生Bug的可能性(代码质量要优于程序性能)
优化原则
优化的目的是为了降低程序的整体开销,先做评估,将重点放在对程序整体开销影响最大的部分,一般是花在循环上。
浏览器花费时间大多数用在DOM上
# 创建快速响应的Web应用(页面响应快准则;内存、GC对响应时间的影响)
“足够快”准则
0.1秒内:用户直接操作UI中对象的感觉极限。对用户是即时的
1秒内:用户随意地在计算机指令空间进行操作而无需过度等待的感觉极限。对用户感觉到计算机处于对指令的“处理中”。
超过10秒:用户专注于任务的极限。需要一个百分比完成指示器,以及一个方便用户中断操作且有清晰标识的方法
一些解决方案
线程处理WebWorker
线程处理不可用,可使用定时器setTimeout替代,但有可能不可行,需要额外逻辑处理,不是简单直接就能进行
插曲:xmlHttpRequest有同步模式和异步模式,同步模式下其行为在主线程中执行,影响用户交互的延迟时间,切勿使用同步模式
内存使用对响应时间的影响
应用程序线程与同时运行的GC的互斥性。垃圾收集器要完成其工作,需要在一段时间(stop the world)内防止所有其他线程访问它正在处理的堆空间
操作系统提供两种内存,物理内存(RAM)和虚拟内存(硬盘),内存分页是它们的最小内存单元。
分页导致的性能降低和GC停顿不同之处,分页是导致全面、无处不在的迟钝,而GC停顿是分散、独立的,且随时间而增加停顿时间
解决方案:delete删除对象上的无用属性;删除DOM上无用元素
# 拆分初始化负载(非初始化负载延迟加载)
- 好处:减少初始化下载量,缩短响应时间
- 拆分的标准:onload事件作为分割线,在此之前执行的是初始化代码,在此之后执行的是后加载代码
- 未定义标识符(竞争状态)问题:为每个延迟加载的函数创建一个桩函数(与原函数名称相同但函数体为空),保证用户在函数下载完成前进行交互不报错无响应;或者,记录被触发的桩函数,当函数下载完毕即执行
# 无阻塞加载脚本(允许外部脚本与其他资源并行下载)
是否需要重构代码 | 要求同源 | 是否阻塞渲染 | 是否阻塞onload | 是否保证顺序执行 | |
---|---|---|---|---|
正常script(高版本支持并行下载,是否所有资源类型待验证) | 否 | 否 | 是 | 是 |
xhr + eval | 是 | 是 | 否 | 否 |
(R)xhr + 创建script元素注入脚本内容 | 否 | 是 | 否 | 否 |
iframe元素请求内联脚本的HTML文档 | 是 | 是 | 否 | 是 |
(R)动态创建script | 否 | 否 | 否 | 是 |
(R)script标签defer属性 | 否 | 否 | 是 | 是 |
document.write写入脚本元素(仅旧版本支持) | 否 | 否 | 是 | 是 |
注:以上几种解决方案需要在最新浏览器Chrome重新实践
# 整合异步脚本(并行下载下保证存在依赖性的外部脚本、行内代码之间顺序执行)
问题1:无阻塞加载脚本部分解决方案存在异步问题,在行内代码使用到外部脚本的场景中容易出现竞争状态
解决方案:异步加载时保持执行顺序
硬编码回调:外部脚本调用行内脚本中的函数(与“动态创建script”方案配合)
window.onLoad:监听该事件,执行行内代码,利用部分无阻塞加载脚本方案(包括“动态创建script|script标签defer属性|iframe元素请求内联脚本的HTML文档|document.write”)会阻塞onload这点。缺点是行内代码延迟执行,因为外部脚本可能已经加载执行了,但是当中又加载了其他资源;仅适用于渲染无阻塞但阻塞onload的加载脚本方式
定时器:定时轮询外部脚本是否加载。缺点是时间间隔设置太短增加开销,设置太长产生不必要的延时;需要处理外部脚本加载失败时导致轮询死循环,外部脚本特定标识变化的情况,代码维护成本更高
(R)script.onLoad:监听外部脚本加载完毕事件,执行行内脚本代码。此方案维护简单(与“动态创建script”方案配合)
降级使用script标签:使用script标签包含外部脚本和行内代码,如
<script src="./outer.js"> // inner code </script> <!-- 与“动态创建script”方案配合 --> <script> // inner code... function main(){ // inner code... } var script = document.createElement('script'); script.src = './outer.js'; script.innerHTML = 'main();'; document.querySelector('head').appendChild(script); </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16只用一个script标签,更干净;行内代码对外部脚本的依赖一目了然,更清晰;若外部脚本加载失败,内部代码不执行,避免未定义标识符报错,更安全。无需事件监听,开销小。不足之处是浏览器并不支持该写法,会执行外部脚本,忽略行内代码。为此需要在外部脚本添加额外代码进行处理,
var targetScript = document.querySelector("script[src='./outer.js']"); if(targetScript){ eval(targetScript.innerHTML); }
1
2
3
4
问题2:当有多个外部脚本相互依赖时,也会出现竞争状态,如何解决?
解决方案
- 管理XHR注入(仅限于脚本与页面同源):
// 保证顺序的异步加载的脚本队列 scriptQueues = []; // 加载管理xhr注入的脚本 function loadScriptXhrInjection(url, onload){ var len = scriptQueues.length; var scriptCfg = { response: null, onload, done: false }; scriptQueues[len] = scriptCfg; var xhrObj = new XMLHttpRequest(); xhrObj,onreadystatechange = function(){ if(xhrObj.readyState === 4){ scriptQueues[len].response = xhrObj.responseText; injectScript(); } } xhrObj.open('GET',url,true); xhrObj,send(); } // 注入脚本 function injectScript(){ var len = scriptQueues.length; for(var i = 0; i < len; i++){ var script = scriptQueues[i]; if(!script.done){ if(!script.response){ // 停止剩余脚本的注入 break; } else { // 优化项,重复注入脚本问题? eval(script.response); script.done = true; } } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38- 动态创建script或document写入脚本(支持脚本与页面不同源):
// 加载动态创建的脚本(仅限于FireFox、Opera浏览器,是顺序执行) // 优化项,是否可以管理XHR注入一样,做一层管理? function loadScriptDOMElement(url, onload){ var script = document.createElement('script'); script.src = url; if(onload){ script.onloadDone = false; script.onload = onload; script.onreadystatechange = function(){ if(['loader','complete'].includes(script.readyState) && !script.onloadDone){ script.onloadDone = true; script.onload(); } } } document.querySelector('head').appendChild(script); } // 加载文档写入的脚本(默认顺序执行) function loadScriptDocWrite(url, onload){ document.write(`<script src='${url}'></script>`); if(onload){ window.addEventListener('load', onload, false); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24问题1、2的综合解决方案(当有多个外部脚本相互依赖,且后有行内代码依赖外部脚本时,该场景下需要我们整合前面的方案出来)
单个脚本:直接调用loadScriptDOMElement即可
多个脚本:同源使用loadScriptXhrInjection,异源使用loadScriptDocWrite或"管理动态创建脚本注入"
function loadScripts(urls, onload, dependencyIdx){ var len = urls.length; var diff = false; for(var i = 0; i < len; i++){ if(!sameDomain(urls[i])){ diff = true; break; } } var loadFunc = loadScriptXhrInjection; if(diff){ // 优化项,此处建议使用管理动态创建脚本注入 loadFunc = loadScriptDocWrite; } for(var i = 0; i < len; i++){ loadFunc(urls[i], dependencyIdx === i? onload: null); } } function sameDomain(url){ var diff = false; var current = location.protocol + '//' + location.host; if(url.indexOf(current) === -1){ diff = true; } return !diff; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 布置行内脚本
问题:行内脚本阻塞资源并行下载和逐步渲染(浏览器已优化)解决方案行内脚本置于文档底部异步(使用定时器或onload事件监听)执行行内脚本脚本声明defer属性
在同优先级下,样式应用的顺序按照代码书写的顺序,而不按照下载快慢的顺序问题:样式表阻塞行内脚本(浏览器已优化)原因:行内脚本可能含有依赖于样式表中样式的代码,如getElementByClassName解决方案调整行内脚本位置,使其不出现在样式表和任何其他资源之间
# 编写高效的Javascript
性能优化除了关注页面加载时间,也要关注交互响应速度
没有银弹:没有绝对有效的方式
函数执行时查找变量的过程及其对性能的影响
函数执行时,JS引擎需要解析函数里的标识符。解析过程是检查作用域链中的每个对象,直至找到指定的标识符。查找从作用域链中的第一个对象开始,这个对象就是包含该函数局部变量的活动对象(包含了this、arguments、命名参数、该函数所有的局部变量),若在活动对象中没找到标识符,则继续在作用域链的下一个对象里查找标识符,一旦找到,查找结束。
在作用域链中查找的对象个数直接影响标识符解析的性能,标识符在作用域链中的位置越深,查找和访问它所需的时间就越长,若作用域管理不当,对脚本执行时间带来负面影响
编写高效Javascript的建议
使用局部变量:存取非局部变量比局部变量耗时要多,若存取非局部变量的操作超过一次,则为了降低性能损耗,将其存储到局部变量中。另外,未使用var声明的变量默认为全局变量。(变量在作用域链中被访问越深,脚本执行更耗时,但对使用V8 JS引擎的浏览器的影响是微乎其微)
避免使用增长作用域链的结构:比如with语句(废弃)、try-catch语句(对使用V8 JS引擎的浏览器的影响是微乎其微)
数组、对象数据的存取能用字面量、局部变量替代的则用:数组、对象需要根据索引或属性查找数据存储的位置,开销相较于字面量、变量要大,且随着数据结构深度增加,对数据存取速度影响也变大。(实际在Chrome浏览器实践中体现并不明显,即使数据很大,可能已经做了优化)
// 这是一个遍历拥有一万个元素的数组示例,其中a段将数组长度用局部变量抽出,b段则每次遍历时重新获取一次。按理a段执行时间应该比b段短,然而在V8引擎中并非如此,反而慢了 const len = 10000; const initialValue = ''; const arr = new Array(len).fill(initialValue, 0 , len); // const arr = Array.from(new Array(len),function(){return initialValue}); let m = 0; console.time('a'); for(let j = 0, len = arr.length; j < len; j++){ m++; } console.timeEnd('a'); let n = 0; console.time('b'); for(let k = 0; k < arr.length; k++){ n++; } console.timeEnd('b');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19慎用HTMLCollection对象:对其进行存取操作,会重新查询DOM匹配节点。若操作频繁,可以将操作的数据(比如读取length)存储在局部变量中或者将其成员存储到一个数组再操作。总之,操作DOM对象的开销要比非DOM对象要大
流控制
分支比较多时,可以将if单层改为多层if嵌套(else不进行条件判断),或者使用switch语句代替if语句(各大最新版本的浏览器已经做到第N层与第一层的执行时间几乎接近)。套路是分支少用if,分支多用switch,更大量查询时用数组或对象映射,保证代码可读性,易维护性
使用循环时,建议将循环变量递减到0,而不是递增到数组长度,使结束条件变成是否为0(自动类型转换为false),若是则停止,这减少了比较操作。
数组方法indexOf的耗时可能比普通循环还长。
使用for-in语句比普通循环(for、while、do-while)慢,因为需要从一个对象解析出可枚举属性,它会遍历整个原型链,增加耗时,降低循环性能。
大数组优化手段,展开循环(又称Duff策略),即通过限制循环的次数减少开销,意味每次循环完成多次循环的工作。示例如下,
// 8是最佳数值,不是任意取的 var iterations = Math.ceil(values.length / 8); var startAt = values.length % 8; var i = 0; do { switch(startAt){ case 0: process(values[i++]); case 7: process(values[i++]); case 6: process(values[i++]); case 5: process(values[i++]); case 4: process(values[i++]); case 3: process(values[i++]); case 2: process(values[i++]); case 1: process(values[i++]); } } while(--iterations > 0) // 利用分而治之思想对其进一步优化,将switch语句从do-while语句抽离 var iterations = Math.floor(values.length / 8); var leftOver = values.length % 8; var i = 0; if(leftOver > 0){ do { process(values[i++]) } while(--leftOver > 0); } do { process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); } while(--iterations > 0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
字符串操作
- 字符串连接
// 性能总是最佳的 var txt = 'Hello'; txt += ' '; txt += 'World!'; // 在很久很久以前,使用数组join方法会比直接拼接快,现在肯定不是了 var buffer = [], i = 0; buffer[i++] = 'Hello'; buffer[i++] = ' '; buffer[i++] = 'World!'; var txt = buffer.join('');
1
2
3
4
5
6
7
8
9
10- 字符串裁剪
function trim1(text) { return text.replace(/^\s+|\s+$/g,""); } function trim2(text) { return text.replace(/^\s+/,"").replace(/\s+$/,""); } function trim3(text) { text = text.replace(/^\s+/, ""); for (var i = text.length - 1; i >= 0; i--) { if(/\s/.test(text.charAt(i))) { text = text.substring(0, i + 1); break; } } return text; } // 最佳 String.prototype.trim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18浏览器限制JS可运行最长时间(担心页面冻结而出现假死现象),可能以执行语句的数量(IE)或控制JS引擎执行总时间(火狐浏览器)或内存不足(Google浏览器)作为判断条件。使用定时器或时间分片(requestIdleCallback)或其他手段(优化代码,如是否使用过多DOM操作、循环还是递归)绕开限制,避免浏览器弹出中止运行脚本运行的警告。
// 定时器处理数组元素 function chunk(array, func, context){ var arr = array.concat(); setTimeout(function(){ var item = arr.shift(); func.call(context, item); if(arr.length > 0){ // 使用定时器,调用本函数处理下一个元素 setTimeout(arguments.callee, 100) } }, 100) } // 定时器排序数组元素 function sort(arr, onComplete){ var pos = 0; (function(){ var j, value; for(j = arr.length; j > pos; j--){ if(arr[j] < arr[j - 1]){ value = arr[j]; arr[j] = arr[j - 1]; arr[j - 1] = value; } } pos++; if(pos < arr.length){ // 使用定时器,调用匿名自调用函数排序下一个数组元素 setTimeout(argument.callee, 10); }else{ onComplete(); } })(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33