加载和执行
脚本位置
放在<head>中的javascript文件会阻塞页面渲染:一般来说浏览器中有多种线程:UI渲染线程、javascript引擎线程、浏览器事件触发线程、HTTP请求线程等。多线程之间会共享运行资源,浏览器的js会操作dom,影响渲染,所以js引擎线程和UI渲染线程是互斥的,导致执行js时会阻塞页面的渲染。
最佳实践:所有的Script标签尽可能放在body标签底部,以尽量减少对整个页面下载的影响
组织脚本
每个<script>
标签初始下载时都会阻塞页面渲染,所以应减少页面包含的<script>
标签数量。内嵌脚本放在引用外链样式表的<link>
标签之后会导致页面阻塞去等待样式表的下载,建议不要把内嵌脚本紧跟在<link>
标签之后。外链javascript的HTTP请求还会带来额外的性能开销,减少脚本文件的数量将会改善性能。
无阻塞的脚本
无阻塞脚本的意义在于在页面加载完成后才加载JavaScript代码。(windows对象的load事件触发后)
延迟的脚本
带有defer属性的<script>
标签可以放置在文档的任何位置。对应的Javascript文件将在页面解析到<script>
标签时开始下载,但是并不会执行,知道DOM加载完成(onload事件被触发前)。当一个带有defer属性的JavaScript文件下载时,它将不会阻塞浏览器的其他进程,可以与其他资源并行下载。执行的顺序是script、defer、load。
动态脚本元素
使用JavaScript动态创建HTML中script元素,例如一些懒加载库。
优点:动态脚本加载凭借着它在跨浏览器兼容性和易用的优势,成为最通用的无阻塞加载解决方式。
XHR脚本注入
创建XHR对象,用它下载JavaScript文件,通过动态创建script元素,将代买注入页面中
var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
if(xht.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
优点:可以下载JavaScript但不立即执行,在所有主流浏览器中都可以正常工作。
缺点:JavaScript文件必须与所请求的页面处于相同的域,意味着文件从CDN下载。
数据存取
存储的位置
数据存储的位置会很大程度上影响读取速度。
- 字面量:字面量只代表自身,不存储在特定的位置。包括:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined。(个人理解:对象的指针本身是字面量)。
- 本地变量:var定义的数据存储单元。
- 数组元素:存储在JavaScript数组内部,以数字为引。
- 对象成员:存储在JavaScript对象内部,以字符串作为索引。
大多数情况下从一个字面量和一个局部变量中存取数据的差异是微不足道的。访问数据元素和对象成员的代价则搞一点。如果在乎运行速度,尽量使用字面量和局部变量,减少数组和对象成员的使用。
管理作用域
作用域链
每个JavaScript函数都表示为一个对象,更确切得说是Function对象的一个实例。它也有仅供JavaScript引擎存储的内部属性,其中一个内部属性是[[Scope]]
,包涵了一个被创建的作用域中对象的集合,即作用域链。作用雨量决定哪些数据能被函数访问。作用域中的每个对象被称为一个可变对象。
当一个函数被创建后,作用域链会被创建函数的作用域中可访问的数据对象所填充。执行函数时会创建一个称为执行上下文的内部对象。执行上下文定义了函数执行时的环境。每次函数执行时对应的执行环境都是独一无二的,多次调用同一个函数也会创建多个执行上下文,当函数执行完毕,执行上下文会被销毁。每个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建时,它的作用域链初始化成当前运行函数的[[scope]]
属性中的对象。这些值哪找它们出现在函数中的顺序,被复制到执行环境的作用域链中。这个过程一旦完成,一个被称为活动对象的新对象就为执行上下文创建好了。
活动对象作为函数运行时的变量对象,包含了所有局部对象,命名函数,参数集合以及this
。然后次对象被推入作用域链的最前端。当执行环境被销毁时,活动对象也随之销毁。执行过程中每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。搜索过程中从作用域链头部开始,也就是当前运行函数的活动对象。如果找到,就是用这个标识符对应的变量,如果没找到,继续搜索作用域链的下一个对象直到找到,若无法搜索到匹配的对象,则标识符被当做未定义的。这个搜索过程影响了性能。
标识符解析的性能
一个标识符所在的位置越深,读写速度就越慢,全局变量总是存在于执行环境作用域的最末端,因此它是最深的。
最佳实践:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。
改变作用域链
一般来说一个执行上下文的作用域链是不会改变的。但是,with
语句和try-catch
语句的catch
子语句可以改变作用域链。
with
语句用来给对象的所有属性创建一个变量,可以避免多次书写。但是存在性能问题:代码执行到with
语句时,执行环境的作用域链临时被改变了,创建了一个新的(包含了with对象所有属性)对象被创建了,之前所有的局部变量现在处于第二个作用域链对象中,提高了访问的代价。建议放弃使用with
语句。
try-catch
语句中的catch
子句也可以改变作用域链,当try
代码块中发生错误,执行过程会自动跳转到catch
子句,把异常对象推入一个变量对象并置于作用域的首位,局部变量处于第二个作用域链对象中。简化代码可以使catch
子句对性能的影响降低。
最佳实践:将错误委托给一个函数来处理。
动态作用域
无论with
语句还是try-catch
语句的子句catch
子句、eval()
语句,都被认为是动态作用域。经过优化的JavaScript引擎,尝试通过分析代码来确定哪些变量是可以在特定的时候被访问,避开了传统的作用域链,取代以标识符索引的方式快速查找。当涉及动态作用域时,这种优化方式就失效了。
最佳实践:只在确实有必要时使用动态作用域。
闭包、作用域和内存
由于闭包的[[Scope]]
属性包含了与执行上下文作用域链相同的对象的引用,因此会产生副作用。通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在闭包的[[Scope]]
属性中,因此激活对象无法被销毁,导致更多的内存开销。
最需要关注的性能点:闭包频繁访问跨作用域的标识符,每次访问都会带来性能损失。
最佳实践:将常用的跨作用域变量存储在局部变量中,然后直接访问局部变量。
对象成员
无论是通过创建自定义对象还是使用内置对象都会导致频繁的访问对象成员。
原型
JavaScript中的对象是基于原型的。解析对象成员的过程与解析变量十分相似,会从对象的实例开始,如果实例中没有,会一直沿着原型链向上搜索,直到找到或者到原型链的尽头。对象在原型链中位置越深,找到它也就越慢。搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上遍历原型链带来的开销,这让性能问题更为严重。
嵌套成员
对象成员可能包含其他成员,每次遇到点操作符"."会导致JavaScript引擎搜索所有对象成员。
缓存对象成员值
由于所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用他们,只在必要时使用对象成员,例如,在同一个函数中没有必要多次读取同一个对象属性(保存到局部变量中),除非它的值变了。这种方法不推荐用于对象的方法,因为将对象方法保存在局部变量中会导致this
绑定到window
,导致JavaScript引擎无法正确的解析它的对象成员,进而导致程序出错。
DOM编程
浏览器中的DOM
文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的程序接口API。DOM是个与语言无关的API,在浏览器中的接口是用JavaScript实现的。客户端脚本编程大多数时候是在和底层文档打交道,DOM就成为现在JavaScript编码中的重要组成部分。浏览器把DOM和JavaScript单独实现,使用不同的引擎。
天生就慢
DOM和javascript就像两个岛屿通过收费桥梁连接,每次通过都要缴纳“过桥费”。
推荐的做法是尽可能减少过桥的次数,努力待在ECMAScript岛上。
DOM访问与修改
访问DOM元素是有代价的——前面的提到的“过桥费”。修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化(重排)。最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。
在循环访问页面元素的内容时,最佳实践是用局部变量存储修改中的内容,在循环结束后一次性写入。
通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript中处理。
节点克隆
大多数浏览器中使用节点克隆都比创建新元素要更有效率。
选择API
使用css
选择器也是一种定位节点的便利途径,浏览器提供了一个名为querySelectorAll()
的原生DOM方法。这种方法比使用JavaScript和DOM来遍历查找元素快很多。使用另一个便利方法——querySelector()
来获取第一个匹配的节点。
重绘与重排
浏览器下载完页面中的所有组件——HTML标记、JavaScript、CSS、图片——之后会解析并生成两个内部的数据结构:DOM树(表示页面结构)、渲染树(表示DOM节点如何显示)。当DOM的变化影响了元素的几何属性,浏览器会使渲染树中受到影响的部分失效,并重构,这个过程成为重排,完成后,会重新绘制受影响的部分到屏幕,该过程叫重绘。并不是所有的DOM变化都会影响几何属性,这时只发生重绘。重绘和重排会导致web应用程序的UI反应迟钝,应该尽量避免。
重排何时发生
当页面布局的几何属性改变时就需要重排:
- 添加或删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变(包括:外边据、内边距、边框厚度、宽度、高度等属性改变)
- 内容改变,例如:文本改变或图片被另一个不同尺寸的图片代替
- 页面渲染器初始化
- 浏览器窗口尺寸改变
渲染树变化的排队与刷新
由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。但是有些操作会导致强制刷新队列并要求任务立刻执行:
1. offsetTop,offsetLeft,offsetWidth,offsetHeight
2. scrollTop,scrollLeft,scrollWidth,scrollHeight
3. clientTop,clientLeft,clientWidth,clientHeight
4. getComputedStyle()
以上属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的修改变化并触发重排以返回正确的值。
最佳实践:尽量将修改语句放在一起,查询语句放在一起。
最小化重绘和重排
为了减少发生次数,应该合并多次DOM的样式的修改,然后一次处理掉。
批量修改DOM
当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:
- 使元素脱离文档
- 对其应用多重改变
- 把元素带回文档流
该过程会触发两次重排——第一步和第三步,如果忽略这两步,在第二步所产生的任何修改都会触发一次重排。
有三种基本的方法可以使DOM脱离文档:
- 隐藏元素,应用修改,重新显示
- 使用文档片段,在当前DOM之外构建一个子树,再把它拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
推荐使用文档片段,因为它们所产生的DOM遍历和重排次数最少。
缓存缓存布局信息
当你查询布局信息时,浏览器为了返回最新值,会刷新队列并应用所有变更。
最佳实践:尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后操作局部变量。
让元素脱离动画流
用展开、折叠的方式来显示和隐藏部分页面是一种常见的交互模式。通常包括展开区域的几何动画,并将页面其他部分推向下方。一般来说,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。当一个动画改变整个页面的余下部分时,会导致大规模重排。节点越多情况越差。避免大规模的重排:
1. 使用绝对定位页面上的动画元素,将其脱离文档流。
2. 应用动画
3. 当动画结束时回恢复定位,从而只会下移一次文档的其他元素。
这样只造成了页面的一个小区域的重绘,不会产生重排并重绘页面的大部分内容。
:hover
如果有大量元素使用了:hover,那么会降低响应速度。此问题在IE8中更为明显。
事件委托
当页面中存在大量元素,并且每一个都要一次或多次绑定事件处理器时,这种情况可能会影响性能,每绑定一个事件处理器都是有代价的,它要么加重了页面负担(更多的代码、标签),要么增加了运行期的执行时间。需要访问和修改的DOM元素越多,应用程序就越慢,特别是事件绑定通常发生在onload时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理事件,而且浏览器要跟踪每个事件处理器,这也会占用更多的内存。这些事件处理器中的绝大部分都可能不会被触发。
事件委托原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
根据DOM标准,每个事件都要经历三个阶段:
1. 捕获
2. 到达目标
3. 冒泡
IE不支持捕获,但是对于委托而言,冒泡已经足够。
<body>
<div>
<ul id="menu">
<li>
<a href="menu1.html">menu #1</a>
</li>
<li>
<a href="menu1.html">menu #2</a>
</li>
</ul>
</div>
</body>
在以上的代码中,当用户点击链接“menu #1”,点击事件首先从a标签元素收到,然后向DOM树上层冒泡,被li标签接收然后是ul标签然后是div标签,一直到达document的顶层甚至window。
委托实例:阻止默认行为(打开链接),只需要给所有链接的外层UL"menu"元素添加一个点击监听器,它会捕获并分析点击是否来自链接。
document.getElementById('menu').onclick = function(e) {
//浏览器target
e=e||window.event;
var target = e.target||e.srcElement;
var pageid,hrefparts;
//只关心hrefs,非链接点击则退出,注意此处是大写
if (target.nodeName !== 'A') {
return;
}
//从链接中找出页面ID
hrefparts = target.href.split('/');
pageid = hrefparts[hrefparts.length-1];
pageid = pageid.replace('.html','');
//更新页面
ajaxRequest('xhr.php?page='+id,updatePageContents);
//浏览器阻止默认行为并取消冒泡
if (type of e.preventDefault === 'function') {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue=false;
e.cancelBubble=true;
}
};
跨浏览器兼容部分:
1. 访问事件对象,并判断事件源
2. 取消文档树中的冒泡(可选)
3. 阻止默认动作(可选)
算法和流程控制
循环
循环的类型
ECMA-262标准第三版定义了javascript的基本语法和行为,其中共有四种循环。
-
第一种是标准的for循环。for循环是javascript最常用的循环结构,直观的代码封装风格被开发者喜爱。它由四部分组成:初始化、前测条件、后执行体、循环体。
for (var i=0;i<10;i++){ //do something }
while循环。while循环是最简单的前测循环,由一个前测条件和一个循环体构成。
do-while循环是javascript唯一一种后测循环,由一个循环体和一个后测条件组成,至少会执行一次。
for-in循环。可以枚举任何对象的属性名。
循环的性能
JavaScript提供的四种循环类型中,只有for-in
循环比其他几种明显要慢。因为每次迭代操作会同时搜索实例或原型属性,for-in
循环的每次迭代都会产生更多开销。速度只有其他类型循环的七分之一。除非你明确需要迭代一个属性数量未知的对象,否则应该避免使用for-in
循环。如果你需要遍历一个数量有限的已知属性列表,使用其他循环类型会更快,比如数组。
除for-in
外,其他循环类型的性能都差不多,类型的选择应该基于需求而不是性能。
提高循环的性能
- 减少每次迭代处理的事务
- 减少迭代的次数