参考:《高性能JavaScript》
一、加载和执行
1. 无论当前的JavaScript代码是内嵌还是包含在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成
2. 浏览器在解析到
标签之前,不会渲染页面的任何部分(因此将脚本放到body标签底部,可以减少对页面的阻塞时间)
3. 确定该脚本不会修改DOM元素,可以使用defer
属性异步加载脚本
-
defer
下载完成后不会马上执行,直到DOM加载完成,onload事件被触发前才执行 -
async
--HTML5的属性,也是异步加载脚本,一加载完就执行
4. ==动态脚本==加载凭借着它在跨浏览器兼容性和易用的优势,成为==最通用的无阻塞加载解决方案==
function loadScript(url, callback){
var script = document.createElement("script");
script.type = "text/javascript";
if(script.readyState){ //IE
script.onreadystatechange = function(){
if(script.readyState == "loaded" || script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
}
}else { //其它浏览器
script.onload = function(){
callback();
}
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
5. 常见无阻塞脚本加载工具:
- YUI3
- LazyLoad类库
- LABjs
小结
减少对JavaScript对页面加载性能影响的几种办法:
-
标签闭合之前,将所有的
标签放到页面底部。这能确保在脚本执行前已经完成渲染。
- 合并脚本。页面的
标签越少,加载也就越快,响应也更迅速。无论外链还是内嵌脚本都是如此。
- 有多种无阻塞下载JavaScript的方法:
- 使用
标签的defer
或async
属性; - 使用动态创建的
元素来下载并执行代码; - 使用XHR对象下载JavaScript代码并注入页面中。
- 使用
二、数据存取
1. 四种基本的数据存取位置
- 字面量:
- 字面量只代表本身,不存储在特定位置。JavaScript中的字面量有:字符串、数字、布尔值、对象、数组、函数、正则表达式,以及特殊的null和undefined值。
- 本地变量:
- 开发人员使用关键字
var
定义的数据存储单元。
- 开发人员使用关键字
- 数组元素:
- 存储在JavaScript数组对象内部,以数字作为索引。
- 对象成员:
- 存储在JavaScript对象内部,以字符串作为索引。
字面量和局部变量的访问速度快于数组项和对象成员的访问速度。
2. 在执行环境的作用域中,一个标识符所在的位置越深,它的读写速度也就越慢
如果跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
3. 动态作用域
- with:
- 当代码执行到
with
语句时,执行环境的作用域链临时被改变了。一个新的变量对象被创建,它包含了参数指定的对象的所有属性。这个对象被推入作用域链的首位。
- 当代码执行到
- try-catch:
- 当
try
代码块中发生错误,执行过程会自动跳转到catch
子句,然后把异常对象推入一个变量对象并置于作用域的首位。 - 一旦
catch
子句执行完毕,作用域链就会返回到之前的状态。
- 当
尽量简化代码来使的
catch
子句对性能的影响最小化,将错误委托给一个函数来处理。
- eval
只有在确实有必要时才推荐使用动态作用域。
4. 原型
- 对象可以有两种成员类型:实例成员(也称own成员)和原型成员。
-
hasOwnProperty()
方法可以判断对象是否包含特定的实例成员。 - 要确定对象是否包含特定的属性,可以使用in操作符,它既会搜索实例也会搜索原型。
-
小结
数据存储的位置会对代码整体性能产生重大的影响。
- 访问字面量和局部变量的速度最快,相反,访问数组元素和对象成员相对较慢。
- 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快。变量在作用域中的位置越深,访问所需要时间就越长。由于全局变量总处于作用域链的最末端,因此访问速度是最慢的。
- 避免使用
with
语句,因为它会改变执行环境作用域链。同样,try-catch
语句中的catch
子句也有同样的影响,因此也要小心。 - 嵌套的对象成员会明显影响性能,尽量少用。
- 属性或方法在原型链中的位置越深,访问它的速度也就越慢。
- 通常来说,你可以==通过把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善JavaScript性能==,因为局部变量访问速度更快。
三、DOM编程
1. 文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的程序接口(API)
2. DOM的渲染和JavaScript引擎是相互独立的。
两个相互独立的东西通过接口连接就会很慢,==要尽可能减少访问次数==。
3. 访问DOM的次数越多,代码的运行速度就越慢。
通用的法则是:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。
4. 在大多数浏览器中,innerHTML
比DOM方法documnet.createElemnt()
要快。
5. 克隆节点element.cloneNode()
比创建节点document.createELemnt()
要快。
6. 访问HTML集合
HTML集合是包含了DOM节点引用的类数组对象,一直和文档保持连接,每次访问都要重复执行查询的过程,是低效之源。
- 只需要遍历一个相对较小的元素,那么缓存
length
就够了。 - 由于遍历数组比遍历集合快,先将集合元素拷贝到数组中,那么访问它的属性会更快。
- 使用局部变量替代这些需要多次读取的元素
7. 遍历DOM
- IE中,
nextSibling
比childNode
表现优异。 - 直接使用元素节点选择器查询元素,
children
比chidNodes
快。 - 使用选择器API,
querySelectorALl()
比使用JavaScript和DOM来遍历查找元素要快。
8. 重绘和重排
- 当DOM变化影响元素的宽高和布局改变就会引发浏览器“重排(reflow)”。
- 何时触发
- 添加或删除可见的DOM元素。
- 元素位置的改变。
- 元素的尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变)。
- 内容改变,例如:文本改变或图片被另一个不同尺寸的图片代替。
- 页面渲染器初始化。
- 浏览器窗口尺寸改变。
- 滚动条出现。
- 手动触发,最好避免使用的属性
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStye() (currentStyle in IE)
- 何时触发
- 浏览器重新绘制受影响的部分到屏幕中,该过程称为“重绘(repaint)”。
9. 最小化重绘和重排
- 改变样式
合并所有的改变然后一次处理,这样只会修改DOM一次。使用
cssText
属性可以实现
- 批量修改DOM
当你需要对DOM元素进行一系列操作时,可通过以下步骤来减少重绘和重排的次数:
- 使元素脱离文档流
- 隐藏元素,应用修改,重新显示。
- 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。
- 对其应用多重改变。
- 把元素带回文档中。
10. 让元素脱离动画流
使用以下步骤可以避免页面中的大部分重排:
- 使用绝对位置来定位页面上的动画元素,使其脱离文档流。
- 让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。
- 当动画结束时恢复定位,从而只会下移一次文档的其他元素。
11. 事件委托
根据DOM标准,每个事件都要经历三个阶段:
- 捕获
- 到达目标
- 冒泡
小结
访问和操作DOM是现代Web应用的重要部分。但是每次穿越连接ECMAScript和DOM两个岛屿之间的桥梁,都会被收取“过桥费”。为了减少DOM编程带来的性能损失,请记住以下几点:
- 最小化DOM访问次数,尽可能在JavaScript端处理。
- 如果需要多次访问某个DOM节点,请使用局部变量存储它的引用。
- 小心处理HTML集合,因为它实时联系着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。
- 如果可能的话,使用速度更快的API,比如
querySelectorAll()
和firstElementChild
。 - 要留意重绘和重排;批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数。
- 动画中使用绝对定位,使用拖放代理。
- 使用事件委托来减少事件处理器的数量。
四、算法和流程控制
1. 循环
-
循环类型:
-
for
循环:由四部分组成:初始化、前侧条件、后执行条件、循环体。 -
while
循环:最简单的前侧循环,由一个前侧条件和一个循环体构成。任何for
都能改写成while
循环,反之亦然 -
do-while
:do-while
是JavaScript中唯一一种后侧循环,由循环体和后侧条件构成。 -
for-in
循环: 枚举任何对象的属性名。所包含的属性包括对象实例属性以及从原型链继承而来的属性。for-in
循环比其它几种明显要慢,除非你需要迭代一个属性数量未知的对象,否则应该避免使用for-in
循环
-
-
循环性能:
- 优化循环:
- 减少迭代工作量:
- 只查找一次属性,并把值存储到一个局部变量,然后在控制条件中使用这个变量。
- 通过颠倒数组的顺序来提高循环性能。
- 减少迭代次数: 达夫设备(Dufff's Device)
var i = item.length % 8; while(i){ process(item[i--]); } i = Math.floor(item.length / 8); while(i){ process(item[i--]); process(item[i--]); process(item[i--]); process(item[i--]); process(item[i--]); process(item[i--]); process(item[i--]); process(item[i--]); }
- 减少迭代工作量:
- 优化循环:
在所有情况下,基于循环的迭代比基于函数的迭代(
forEach()
)快8倍
2. 条件语句
-
if-else
对比switch
: 大多数情况下switch
比if-else
运行的要快,但只有当条件数量很大时才快的明显。当条件数量较少时使用if-else
,在条件数量较大时使用switch
。- 优化
if-else
: 最小化到达正确分支前所需判断的条件数量。最简单的优化方法是确保最可能出现的条件放在首位。另一种减少条件判断次数的方法是把if-else
组织成一系列嵌套的if-else
语句。(使用二分法把值域分成一系列的区间)
- 优化
3. 查找表(数组,对象)
当有大量离散值需要测试时,if-else
和switch
比使用查找表慢很多。
4. 递归
为了能在浏览器中安全地工作,建议改用迭代、Memoization,或者结合两者使用。
- 迭代: 任何递归能实现的算法同样可以用迭代实现。
- Memoization
/**
* @params {function} fundamental 需要增加缓存的函数
* @params {object} cache 可选的缓存对象
* @return {function} shell 加入缓存功能的函数
**/
function memoize(fundamental, cache){
cache = cache || {};
var shell = function(arg){
if(!cache.hasOwnProperty(arg)){
cache[arg] = fundamental(arg);
}
return cache[arg];
}
return shell;
}
小结
JavaScript和其它编程语言一样,代码的写法和算法的会影响运行时间。于其他语言不同的是,JavaScript可用资源有限,因此优化技术更为重要。
-
for
、while
、do-while
循环性能特性相当,并没有一种循环类型明显快于或慢于其他类型。 - 避免使用
for-in
循环,除非你需要遍历一个属性数未知的对象。 - 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
- 通常来讲,
switch
总是比if-else
快,但并不总是最佳解决方案。 - 当判断条件较多时,使用查找表比
if-else
和switch
更快。 - 浏览器的调用栈大小限制了递归算法在JavaScript中的应用;栈溢出错误会导致其它代码中断运行。
- 如果你遇到栈溢出错误,可将方法改为迭代算法,或使用Memoization来避免重复计算。
运行的代码数量越大,使用这些策略所带来的性能提升也就越明显。
五、字符串和正则表达式
小结
密集的字符串操作和草率地编写正则表达式可能产生严重的性能障碍,本章提供的建议会帮助你避免这些常见的陷阱。
- 当连接数巨大或尺寸巨大的字符串,数组项合并是唯一在IE 7及更早版本中性能合理的方法。
- 如果不需要考虑IE 7及更早版本的性能,数据项合并是最慢的字符串连接方法之一。推荐使用简单的+和+=操作符代替,避免不必要的中间字符串。
- 回溯既是正则表达式匹配功能的基本组成部分,也是正则表达式的低效之源。
- 回溯失控发生在正则表达式本应快速匹配的地方,但是因为某些特殊的字符串匹配动作导致运行缓慢甚至浏览器崩溃。避免这个问题的办法是:是相邻的字元互斥,避免嵌套量词对同一字符串的相同部分多次匹配,通过重复利用预查的原子组去除不必要的回溯。
- 提高正则表达式效率的各种技术手段会有助于正则表达式更快地匹配,并在非匹配位置上花更少的时间(参见:“更多提高正则表达式效率的方法”)。
- 正则表达式并不总是完成工作的最佳工具,尤其当你值搜索字面字符串的时候。
- 尽管有许多方法可以去除字符串的首位空白,但使用两个简单的正则表达式(一个用来去除头部空白,一个用来去除尾部空白)来处理大量字符串内容能提供一个简洁而跨浏览器的方法。从字符串末尾开始循环向前搜索第一个非空白字符串,或者将此技术同正则表达式结合起来,会提供一个更好的替代方案,它很少受到字符串长度影响。
六、快速响应的用户界面
1. 浏览器UI线程
- 大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。
- 长时间的JavaScript代码执行会导致用户界面更新不及时,对用户体验造成不良影响。
- 浏览器会限制JavaScript的运行时间,达到一定限度时会终止JavaScript的运行。
- JavaScript的运行时间不应该超过100毫秒。
- 最理想的方法是让出UI线程的控制权,使得UI可以更新。
2. 浏览器UI线程
- 使用定时器让出时间片段,定时器于UI线程的交互方式有助于把运行耗时较长的脚本拆分为较短的片段。
- 使用定时器处理数组
- 满足两个条件:
- 处理过程不需要同步
- 数据处理不需要按顺序
- 满足两个条件:
function processArray(items, process, callback){
var todo = items.concat(); // 克隆原数组
// 建立一个定时器
setTimeout(function(){
process(todo.shift()); // 处理函数
// 数组内如果还有数据,再执行一次当前函数,否则触发回调函数
if (todo.length > 0){
setTimeout(argument.callee, 25);
}else{
callback(items);
}
}, 25);
}
- 分割任务
- 前提条件: 任务可以异步处理而且不影响用户体验造成相关代码错误。
function multistep(steps, args, callback){
var tasks = step.concat(); // 克隆数组
setTimeout(function(){
// 执行下一个任务
var task = tasks.shift();
task.apply(null, args || []);
// 检查是否还要其它任务
if(tasks.length > 0){
setTimeout(arguments.callee, 25);
}else{
callback();
}
}, 25);
}
function saveDocument(id){
var tasks = [openDocumnet, writeText, closeDocument, updateUI];
multistep(tasks, [id], function(){
alert("Save completed!")
});
}
- 记录代码运行时间
- 通常来说批量处理比单个处理要快。改进
porcessArray()
方法:
- 通常来说批量处理比单个处理要快。改进
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);
}
3. Web Worker
- 能使代码独立运行且不占用浏览器UI线程的时间。
- 其功能是JavaScript的一个子集,由如下部分组成:
- 一个
navigator
对象,只包含四个属性:appName
、appVersion
、user Agent
和platform
。 - 一个
location
对象(于window.loacation
相同,不过所有属性都是只读的)。 - 一个
self
对象,指向worker对象。 - 一个
importScripts()
方法,用来加载Worker所用到的外部JavaScript文件。 - 所有的ECMAScript对象,诸如:
Object
、Array
、Date
等。 - XMLHttpRequest构造器。
-
setTimeout()
和setIntervar()
方法。 - 一个
close()
方法,它能立刻停止Worker运行。
- 一个
- 与Worker通信
- 使用消息系统,消息系统是网页和Worker通信的唯一途径。
var worker = new Worker("code.js");
// 接受信息的事件处理器
worker.onmessage = function(event){
alert(event.data); //使用data属性存放传入的数据
};
worker.postMessage("Nicholas"); // 发送数据给worker
// 加载外部文件
importScripts("file1.js", "file2.js");
self.onmessage = function(event){
self.postMessage("Hello" + event.data + "!");
}
- 实际应用
- Web Workers适用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本。例如:
- 编码/解码大字符串。
- 复杂数学运算(包括图像或视频处理)。
- 大数组排序。
- Web Workers适用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本。例如:
小结
JavaScript和用户界面更新在同一个进程中运行,因此一次只能处理一件事。这意味着当JavaScript代码正在运行时,用户界面不能响应输入,反之亦然。高效的管理UI线程就是要确保JavaScript不能运行太长时间,以免影响用户体验。最后,请牢记如下几点:
- 任何JavaScript任务都不应该执行超过100毫秒。过长的运行时间会导致UI更新出现明显的延迟,从而对用户体验产生负面影响。
- JavaScript运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript长时间运行将导致用户体验变得混乱和脱节。
- 定时器可用来安排代码延迟执行,它使得你可以把长时间运行脚本分解成一系列的小任务。
- Web Worker是新版浏览器支持的特性,它允许你在UI线程外部执行JavaScript代码,从而避免锁定UI。
Web应用越复杂,积极主动地管理UI线程就越重要。即使JavaScript代码再重要,也不应该影响用户体验。
七、Ajax
1. 数据请求技术
- XMLHttpRequest(XHR)
- Dynamic script tag insertion 动态脚本注入
- iframes
- Comet
- MultipartXHR
XMLHttpRequest
- 允许异步发送和接受数据
- 能精确地控制发送请求和数据接受
var url = '/data.php';
var params = [
'id=7894',
'limit=20'
];
var req = new XMLHttpRequest();
req.onreadystatechange = function(){
if(req.redyState === 4){
var responseHeaders = req.getAllResponseHeaders(); // 获取响应头信息
var data = req.reponseText; // 获取数据
// 数据处理
}
req.open('GET', url + '?' + params.join('&'), ture);
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 设置请求头信息
req.send(null); // 发送一个请求
}
POST和GET对比:当只获取数据时,因使用GET请求的数据会被浏览器缓存,若果多次请求同一数据的话,它有助于提升性能;当请求的URL加上参数的长度大于等于2048个字符时,才应该使用POST获取数据(浏览器会限制URL长度)。
动态脚本注入
- 克服了XHR的最大限制:它能跨域请求数据。
- 限制:
- 不能设置请求的头信息。
- 参数传递只能用GET。
- 不能设置请求的超时处理和重试。
- 必须等待所有数据都已经返回才可以访问他们,
- 不能访问请求的头信息,也不能把整个响应消息作为字符串来处理。
var scriptElement = doucment.createElement('script');
scriptElement.src = "http://any-domain.com/javascropt/lib.js";
document.getElementsByTagName('head')[0].appendChild(scriptElement);
function jsonnCallback(jsonString){
var = eval('('+jsonString+')');
// 处理数据...
}
// lib.js
jsonCallback({ "status": 12, "colors": [ "#fff", "#000", "#ff0000" ] });
速度很快,但是引用外部来源的代码不安全。
Multipart XHR
- multipart XHR(MXHR)允许客户端只用一个HTTP请求就可以从服务端向客户端传送多个资源。
- 服务器将多个资源拼接成一个长字符串,客户端接收到数据后进行拆分,可以在收到资源时就立刻处理。
- 缺点:这种方式获取的资源不能被浏览器缓存。
- 在特定场合下使用能显著提升页面的整体性能:
- 页面包含了大量其他地方用不到的资源(因此也无需缓存),尤其是图片。
- 网站已经在每个页面中使用一个独立打包的JavaScript或CSS文件以减少HTTP请求;因为对每个页面来说这些文件都是唯一的,所以并不需要从缓存中读取数据,除非重载页面。
var req = new XMLHttpReques();
var getLatestPacketInterval, lastLength = 0;
req.open('GET', 'rollup_images.php', ture);
req.onreadystatechange = readyStateHandler;
req.send(null);
function readyStateHandler(){
if(req.readyState === 3 && getLatestPacketInterval == null){
// 开始轮询
getLatestPacketInterval = window.setInterval(function(){
getLatestPacket();
}, 15);
}
if(req.readyState === 4){
// 停止轮询
clearInterval(getLatestPacketInterval);
// 获取最后一个数据包
getLatestPacket();
}
}
function getLatestPacket(){
var length = req.responseText.length;
var packet = req.reponseText.substring(lastLength, length);
processPacket(packet);
lastLength = length;
}
2. 发送数据
- XHR
- 信标(beacons)
XMLHttpRequest
function xhrPost(url, params, callback){
var req = new XMLHttpRequest();
req.onerror = function(){
setTimeout(function(){
xhrPost(url, jparams, callback);
}, 1000);
};
req.onreadystatechange = function(){
if(req.readyState == 4){
if(callback && typeof callback === 'function'){
callback();
}
}
};
req.open('POST', url, ture);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setRequestHeader('Content-Type', params.length);
req.send(params.join('&'));
}
当使用XHR发送数据到服务器时,GEt方式会更快。这是因为,对于少量数据而言,一个GET请求往服务器只发送一个数据包。而一个POST请求,至少要发送两个数据包,一个装在头信息,一个装载POST正文。POST更适合发送大量数据到服务器,因为它不关心额外数据包的数量。
Beacons
- 信标是向服务器回传数据最快且最有效的方式。
- 如果你只关心发送数据到服务器(可能需要极少的返回信息),那么请使用图片信标。
var url = '/status_tarcker.php';
var params = [
'step=2',
'time=12316151'
];
var beacon = new Image();
beacon.src = url + '?' + params.join('&');
beacon.onload = function(){
if(this.width == 1){
// 成功
}eles if(this.width == 2){
// 失败,请重试并创建另一个信标
}
};
beacon.onerror = function(){
// 出错, 稍后重试并创建另一个信标。
};
3. 数据格式
- 常用数据格式:
- XML
- XPath
- JSON
- JSON-P
- HTML
- 自定义格式
- 通常来说数据格式越轻量越好,JSON和字符分割的自定义格式是最好的。如果数据集很大并且对解析时间有要求,那么就从如下两种格式中做出选择:
- JSON-P数据, 使用动态脚本注入获取。它把数据当做可执行JavaScript而不是字符串,解析速度极快。它能跨域使用,但涉及敏感数据时不应该使用它。
- 字符分割的自定义格式,使用XHR或动态脚本注入获取,用
split()
解析。这项技术解析大数据集比JSON-P略快,而且通常文件尺寸更小。
4. Ajax性能指南
- 缓存数据
- 在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存。
- 在客户端,把获取到的信息存储到本地,从而避免再次请求。
- 第一种技术使用简单而且好维护,而第二种则给你最大的控制权。
//本地数据存储:把响应文本保存到一个对象中,以URL为键值作为索引。
var localCache = {};
function xhrRequest(url, callback){
// 检查此URL的本地缓存
if(localCache[url]){
callback.success(localCahce[url]);
return;
}
// 此URL对应的缓存没有找到,则发送请求
var req = new createXhrObject();
req.onerror = function(){
callback.error();
};
req.onreadystatechange = function(){
if(req.readyState == 4){
if(req.responseText === '' || req.status == '404'){
callback.error();
return;
}
// 存储响应文本到本地缓存
localCache[url] = req.responseText;
callback.success(req.responseText);
}
};
req.open("GET", url, true);
req.send(null);
}
// 删除缓存
delete localCache['/user/friendlist/'];
小结
高性能的Ajax包括以下方面: 了解你项目的具体需求,选择正确的数据格式和与之匹配的传输技术。
作为数据格式,纯文本和HTML只适用于特定场合,但它们可以节省客户端的CUP周期。XML被广泛应用而且支持良好,但是它十分笨重而且解析缓慢。JSON是轻量级的,解析速度快(被视为原生代码而不是字符串),通用性与XML相当。字符分割的自定义格式十分轻量,在解析大量数据集时非常快,但需编写额外的服务端构造函数,并在客户端解析。
当从页面当前所处的域下请求数据时,XHR提供了最完善的控制和灵活性,尽管它会把接受到的所有数据当成一个字符串,且这有可能降低解析速度。另一方面,动态脚本注入允许跨域请求和本地执行JavaScript和JSON但是它的接口不那么安全,而且还不能读取头信息或相应代码。Multiple XHR可以用来减少请求数,并处理一个响应的各种文件类型,但是它不能缓存接收到的响应。当需要发送数据时,图片信标是一种简单而有效的方法。XHR还可以用POST方法发送大量数据。
除了这些格式和传输数据,还有一些准则有助于加速你的Ajax:
- 减少请求数,可以通过合并JavaScript和CSS,或使用MHXR。
- 缩短页面的加载时间,页面主要内容加载完成后,用Ajax获取那些次要的问题。
- 确保你的代码错误不会输出给用户,并在服务端处理错误。
- 知道何时使用成熟的Ajax类库,以及何时编写自己的底层Ajax代码。
Ajax为提升你的网站的潜在性能提供了广阔的空间,因为许多网站大量使用异步请求,而且它还提供了一些与它无关的问题的解决方案,比如有过多的资源需要加载。在XHR创造性地使用是一个反应迟钝且平淡无奇的页面与响应快速且高效的页面的区别所在;是一个用户痛恨使用的站点与用户迷恋的站点的区别所在。
八、编程实战
1. 避免双重求值
在JavaScript代码中执行另外一段代码时,都会导致双重求值的性能消耗。
var num1 = 5,
num2 = 6,
// eval()执行代码字符串
result = eval("num1 + num2");
// Function()执行代码字符串
sum = new Function("arg1", "arg2", "return arg1 + arg2");
// setTime()执行代码字符串
setTime("sum = num1 + num2", 100);
// settInterval() 执行代码字符串
setInterval("sum = num1 + num2", 100);
双重求值是一项代价昂贵的操作,它比直接包含的代码执行速度要慢许多。
2. 使用Object/Array直接量
使用直接量是创建对象和数组最快的方式。
3. 避免重复工作
别做无关紧要的工作,别重复做已经完成的工作。
- 延迟加载
function addHandler(target, eventType, handler){
// 覆写现有函数
if(target.addEventListener){ // DOM2 Events
addHandler = function(target, eventType, handler){
target.addEventListener(eventType, handler, false);
};
}else { // IE
addHandler = function(target, evetType, handler){
target.attachEvent("on" + eventType, handler);
};
}
//调用新函数
addHandler(target, eventType, handler);
}
function removeHandler(target, eventType, handler){
// 覆写现有函数
if(target.removeEventListener){ //DOM2 Events
removeHandler = function(target, eventType, handler){
target.removeEventListener(eventType, handler, false);
};
}else{ //IE
removeHandler = function(target, eventType, handler){
target.detachEvent("on" + eventType, handler);
};
}
// 调用新函数
removeHanlder(target, eventType, handler);
}
- 条件预加载
var addHandler = document.body.addEventListenre ?
function(target, efvent, handler){
target.addEventListener(eventTye, handler, false);
}:
function(target, eventType, handler){
target.attachEvent("on" + eventType, handler);
};
var removeHanlder = document.body.removeEventListener ?
function(target, efvent, handler){
target.removeEventListener(eventTye, handler, false);
}:
function(target, eventType, handler){
target.detachEvent("on" + eventType, handler);
};
4. 使用速度快的部分
- 位操作
for(var i = 0; i < length; i < len; i++){
if(i & 1){
className = "odd";
}else{
className = "even";
}
}
- 原生方法
常量 | 值 |
---|---|
Math.E |
E的值,自然对数的底 |
Math.LN10 |
10的自然对数 |
Math.LN2 |
2的自然对数 |
Math.LOG2E |
以2为底的E的对数 |
Math.LOG10E |
以10为底的E的对数 |
Math.PI |
π的常量 |
Math.SQRT1_2 |
1/2的平方根 |
Math.SQRT2 |
2的平方根 |
方法 | 含义 |
---|---|
Math.abs(num) |
返回num的绝对值 |
Math.exp(num) |
返回E的指数 |
Math.log(num) |
返回num的自然对数 |
Math.pow(num) |
返回num的power次幂 |
Math.sqrt(num) |
返回num的平方根 |
Math.acos(x) |
返回x的反余弦值 |
Math.asin(x) |
返回x的反正弦值 |
Math.atan(x) |
返回x的反正切值 |
Math.atan2(y, x) |
返回从X轴到(y,x)点的角度 |
Math.cos(x) |
返回x的余弦值 |
Math.sin(x) |
返回x的正弦值 |
Math.tan(x) |
返回x的正切值 |
小结
JavaScript提出了一些独一无二的性能挑战,这与你组织代码的方式有关。随着Web应用变得越来越高级,包含的JavaScript代码也越来越多,各种模式和反模式也逐渐出现。为了编写更高效的代码,请牢记以下编程实践:
- 通过使用
eval()
和Function()
构造器来避免双重求值带来的性能消耗。同样的,给setTimeout()
和setInterval()
传递函数而不是字符串作为参数。 - 尽量使用直接量创建对象和数组。直接量的创建和初始化都比非直接量形式要快。
- 避免做重复的工作。当需要检测浏览器时,可使用延迟加载或条件预加载。
- 在进行数学计时,考虑使用直接操作数字的二进制形式的位运算。
- JavaScript的原生方法总会比你写的任何代码都要快。尽量使用原生方法。
当本书涵盖了大量的优化技术和方法,把这些方案应用在那些被频繁使用的代码上时,你将会看到显著的性能提升。
九、构建并部署高性能JavaScript应用
小结
构建和部署的过程对基于JavaScript的Web应用的性能有着巨大影响。这个过程中最重要的步骤有:
- 合并JavaScript文件以减少HTTP请求数。
- 使用YUI Compressor压缩JavaScript文件。
- 在服务器端压缩JavaScript文件(Gzip编码)。
- 通过正确设置HTTP响应头来缓存JavaScript文件,通过向文件名增加时间戳来避免缓存问题。
- 使用CDN(Content Delivery NetWork)提供JavaScript文件;CDN不仅可以提升性能,它也为你管理文件的压缩与缓存。
所有这些步骤都应该自动化处理,可以使用公开的工具,比如Apache Ant,也可以使用定制的工具来满足你的特定需求。如果你使得构建工程工作起来,你将会显著提高那些依赖大量JavaScript的Web应用或网站的性能。
十、工具
小结
当网页或Web应用变慢时,分析从网络下载的资源以及分析脚本的运行性能能让你专注于那些最需要优化的地方。
- 使用网络分析工具找出加载脚本和页面中其他资源换的瓶颈,这会帮助你决定哪些脚本需要延迟加载,或者需要进一步分析。
- 尽管传统的经验告诉我们要尽量减少HTTP请求数,但把脚本尽可能延迟加载可以加快页面渲染速度,给用户带来更好的体验。
- 使用性能分析工具找出脚本运行过程中速度慢的地方,检查每个函数所消耗的时间,以及函数被调用的次数,通过调用栈自身提供的一些线索来找出需要集中精力优化的地方。
- 尽管消耗的时间和调用次数通常是数据中最有价值的部分,但仔细观察函数的调用过程,你也许会发现其他优化目标。
这些工具会帮助你深入了解你的代码在那些通常你比较陌生的编程环境下是如何运行的。在开始优化工作之前先使用它们,以确保开发时间在刀刃上。