当浏览器遇到<script>标签时,浏览器会停止处理页面,先执行JavaScript代码,然后再继续解析和渲染页面。同样的情况也发生在使用src属性加载JavaScript的过程中,浏览器必须先花时间下载外连接文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了[注:动态加载JavaScript除外]。
1.1 脚本位置(Script Positioning)
浏览器在解析到<body>标签之前,不会渲染页面的任何部分。把脚本放到页面顶部将会导致明显的延迟,通常表现为显示空白页面。
引用多个js文件时,每个文件必须等到前一个文件下载并执行完成才会开始下载。
IE8、Firefox3.5、Safari 4、Chrome2都允许并行下载JavaScript文件。<script>标签在下载外部资源时不会阻塞其他<script>标签,但会阻塞其他资源的下载,如图片。尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有JavaScript代码下载并执行完成才能继续。【由于脚本会阻塞页面其他资源的下载,因此推荐将所有<script>标签尽可能放到<body>标签的底部,以尽量减少对整个页面的下载的影响】
1.2 组织脚本(Grouping Scripts)
由于每个<script>标签初始下载时都会阻塞页面渲染,所以减少页面包含的<script>标签数量有助于改善这一情况。
提示:Steve Souders 还发现,把一段内嵌脚本放在引用外链接样式表的<link>标签之后会导致页面阻塞去等待样式表的下载。这样做是为了确保内嵌脚本再执行时能获得最精准的样式信息。因此,Souders建议永远不要把内嵌脚本紧跟在<link>标签后面
可以把多个文件合并成一个,这样只需要引用一个<script>标签。
任何网站都可以使用一个把指定文件合并处理后的URL来获取任意数量的YUI文件。如,下面URL包含两个文件
http://yui.yahooapis.com/combo?2.7.0/build/yahoo/yahoo-min.js&2.7.0/build/event/event-min.js
这个URL加载了2.7.0版本的yahoo-min.js和event-min.js文件。这两个文件在服务器上是独立存在的,但通过请求以上网址他们会被合并。原来需要用两个<script>标签分别加载两个文件,而现在用一个<script>标签即可同时加载:
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.7.0/build/yahoo/yahoo-min.js&2.7.0/build/event/event-min.js"></script>
1.3 无阻塞的脚本(Nonblocking Scripts)
减少JavaScript文件大小
限制HTTP请求数仅仅是创建响应迅速的Web应用的第一步。
逐步加载JavaScript文件
无阻塞脚本的秘诀在于,在页面加载完成后才加载JavaScript代码。这意味着在window对象的load事件触发后再下载脚本。
1.3.1 延迟的脚本
Deferred Scripts
HTML 4为<script>标签定义了一个扩展属性:defer。defer属性指明本元素所含的脚本不会修改DOM,因此嗲吗能安全的延迟执行。该属性只有IE4+和Firefox 3.5+的浏览器支持,所以它不是一个理想的跨浏览器的解决方案。在其它浏览器中,defer属性会被直接忽略,因此<script>标签会以默认的方式处理(即会造成阻塞)
<script type="text/javascript" src="file1.js" defer></script>
<html>
<head>
<title>Script Defer Example</title>
</head>
<body>
<script defer>
alert("defer");
</script>
<script>
alert("script");
</script>
<script>
window.οnlοad=function(){
alert("load");
};
</script>
</body>
</html
对应的JavaScript文件将在页面解析到<script>标签时开始下载,但不会执行,直到DOM加载完成(onload事件被触发前)。当一个带有defer属性的JavaScript文件下载时,它不会阻塞浏览器的其他进程,因此这类文件可以与页面中的其他资源并行下载。
任何带有defer属性的<script>元素在DOM完成加载之前都不会被执行,无论内嵌或外链脚本都是如此。如以上代码。
不支持defer属性的浏览器的弹出顺序是“defer”、“script” "load" 而支持defer属性的浏览器上,弹出的顺序是:“script” "defer" "load" 注意:带有defer属性的<script>元素不是跟在第二个后面执行,而是在onload事件处理器执行之前被调用。
1.3.2 动态脚本元素
Dynamic Script Elements
这种技术的重点在于:无论在合适启动下载,文件的下载和执行过程不会阻塞页面其他进程。
提示:通常来讲,把新创建的<script>标签添加到<head>标签里比添加到<body>里更保险,尤其是在页面加载过程中执行代码更是如此。当<body>中的内容没有全部加载完成时,IE可能会抛出一个“操作已终止”的错误信息。
使用动态脚本节点下载文件时,返回的代码通常会立刻执行(除了Firefox和Opera,它们会等待此前所有动态脚本节点执行完毕)
Firefox,Opera,Chrome和Safari 3以上版本会在<script>元素接收完成时触发一个load事件。可以通过侦听此事件来获得脚本加载完成时的状态。
IE支持另外一种实现方式,它会触发一个readstatechange事件。<script>元素提供一个readyState属性,它的值在外连文件的下载过程的不同阶段会发生变化,该属性有五种取值:
uninitialized 初始状态
loading 开始下载
loaded 下载完成
interactive 数据完成下载但尚不可用
complete 所有数据已准备就绪
因IE在标识最终状态的readyState的值时并不一致,故最靠谱的方式是同时检查这两种状态,只要其中任何一个触发,就删除事件处理器(以确保事件不会处理两次)
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(){
script.onload = null;
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
loadScript("test.js",function(){
alert("File is loaded!");
});
函数中使用了特征检查(Feature detection)来决定再脚本处理过程中监听哪个事件。
如果需要的话,可以动态加载尽可能多的JavaScript文件到页面上,但要确保考虑了文件的加载顺序。在所有主流浏览器中,只有Firefox和Opera能保证脚本会按照你指定的顺序执行,其他浏览器将会按照从服务器端返回的顺序下载和执行代码,可以将下载操作串联起来以确保下载顺序
loadScript("file1.js",function(){
loadScript("file2.js",function(){
loadScript("file3.js",function(){
});
});
});
如果文件下载顺序很重要,更好的做法是把它们按正确的顺序合并成一个文件。下载这个文件就能一次获得所有代码(由于这个过程是异步的,因此文件大一点不会有影响)
1.3.3 XMLHttpRequest 脚本注入
XMLHttpRequest Script Injection
另一种无阻塞加载脚本的方法是使用XMLHttpRequest(XHR)对象获取脚本并注入页面中。
var xhr = new XMLHttpRequest();
xhr.open("get","http://localhost:20010/advert/js/pub/adv_d.js",true);
xhr.onreadystatechange = function(){
if(xhr.readyState==4){
if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.reponseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
2XX表示有效响应,304意味着是从缓存读取。如果收到了有效响应,就会创建一个<script>元素,设置该元素的text属性为从服务器接收到的reponseText。这实际上相当于创建一个带有内联脚本的<script>标签,一旦新创建的<script>元素被添加到页面上,代码就会立刻执行然后准备就绪。
这种方法的主要局限性是JavaScript文件必须与所请求的页面处于相同的域,这意味着JavaScript文件不能从CDN下载。因此大型的web应用通常不会采用XHR脚本注入技术。
1.3.4 推荐的无阻塞方式
Recommended Nonblocking Pattern
向页面中添加大量JavaScript的推荐做法只需两步:先添加动态加载所需的代码,然后加载初始化页面所需的剩下的代码。
<script type="text/javascript" src="loader.js"></script>
<script>
loadScript("the-rest.js",function{
Application.init();
});
</script>
把这段代码放到<body>闭合标签之前,好处:1、确保了JavaScript执行过程中不会阻塞页面其他内容的显示
2、当第二个JavaScript文件完成下载时,应用所需的所有DOM结果已经创建完毕,并做好了交互的准备,从而避免了需要另一个事件(比如window.load)来检测页面是否准备好。
另一种方法是把loadScript()函数直接嵌入页面,从而避免多产生一次HTTP请求。此方法建议使用YUI Compressor把初始化代码压缩到最小尺寸
YUI3 的方式
YUI3有一个核心设计理念是:由页面中的少量代码来加载丰富的功能组件。
LazyLoad类库
Yahoo!Search的工程师Ryan Grove创建了一个更为通用的延迟加载工具:LazyLoad(源代码:http://github.com/rgrove/lazyload)。LazyLoad是loadScript()函数的增强版
LazyLoad同样支持下载多个JavaScript文件,并能保证在所有浏览器中都以正常的顺序执行。
加载一个文件:
<script type="text/javascript" src="./js/lazyload-min.js"></script>
<script type="text/javascript">
LazyLoad.js("./js/pub/adv_d.js",function(){
alert("我被加载了");
});
alert("执行");
</script>
加载多个文件:
<script type="text/javascript" src="./js/lazyload-min.js"></script>
<script type="text/javascript">
LazyLoad.js(["./js/pub/adv_d.js","./js/pub/adv_d123.js"],function(){
alert("我被加载了");
});
alert("执行");
</script>
要加载多个JavaScript文件,只需给LazyLoad.js()方法传入一个URL数组
尽管使用这种无阻塞的方式可以动态加载很多文件,但建议尽量减少文件数,因为每次下载仍然是一个独立的HTTP请求,而且回调函数会等待所有文件都下载完成后才会执行。
提示:LazyLoad同样可以动态加载CSS文件,这没有太大的意义,因为CSS文件本已是并行下载,不会阻塞页面的其他进程。
LABjs
另一个开源无阻塞脚本加载工具是Kyle Simpson受Steve Souders的启发而编写的LABjs(http://labjs.com)。改工具试图同时下载尽可能多的代码
<script type="text/javascript" src="./js/LAB.js"></script>
<script type="text/javascript">
$LAB.script("./js/pub/adv_d.js")
.wait(function(){
alert("123");
});
</script>
$LAB.script()方法用来定义需要下载的JavaScript文件,$LAB.wait()用来指定文件下载并执行完毕后所调用的函数。LABjs鼓励链式操作,因此每个方法都会返回一个$LAB对象的引用。要下载多个JavaScript文件,只需链式调用另一个$LAB.script()方法:
<script type="text/javascript" src="./js/LAB.js"></script>
<script type="text/javascript">
$LAB.script("./js/pub/first-fire.js")
.script("./js/pub/the_rest.js")
.wait(function(){
alert("123");
});
</script>
LABjs允许你使用wait()方法来指定哪些文件需要等待其他文件。在前面的例子中,first-fire.js的代码不能保证会在the-rest.js的代码前执行。为了确保这一点,你必须在第一个script()方法后调用wait():
<script type="text/javascript" src="./js/LAB.js"></script>
<script type="text/javascript">
$LAB.script("./js/pub/first-fire.js").wait()
.script("./js/pub/the_rest.js")
.wait(function(){
alert("123");
});
</script>
这时,尽管是并行下载,但first-file.js中的代码肯定能在the-rest.js前面执行
1.4 小结
Sumary
管理浏览器中的JavaScript代码是个棘手的问题,因为代码执行过程会阻塞浏览器的其他进程,比如用户界面绘制。每次遇到<script>标签,页面都必须停下来等待代码下载(如果是外链文件)并行,然后继续处理其他部分。尽管如此,还是有几种方法能减少JavaScript对性能的影响。
<body>闭合标签之前,将所有的<script>标签放到页面底部。这能确保在脚本执行前页面已经完成了渲染。
合并脚本。页面中的<script>标签越少,加载也就越快,响应也更迅速。无论外链文件还是内嵌脚本都是如此。
有多种无阻塞下载JavaScript的方法:
-使用script标签的defer属性(仅适用于IE和Firefox3.5以上版本);
-使用动态创建的<script>元素来下载并执行代码;
-使用XHR对象下载JavaScript代码并注入页面中。