第一部分 加载与运行
<html> <head> <title>Script Example</title> </head> <body> <p> <script type="text/javascript"> document.write("The date is " + (new Date()).toDateString()); </script> </p> </body> </html>
当浏览器遇到一个<script>标签时,正如上面HTML页面中那样,无法预知JavaScript是否在<p>标签中添加内容。因此,浏览器停下来,运行此JavaScript代码,然后再继续解析、翻译页面。同样的事情发生在使用src属性加载JavaScript的过程中。浏览器必须首先下载外部文件的代码,这要占用一些时间,然后解析并运行此代码。此过程中,页面解析和用户交互是被完全阻塞的。
为了保持代码的相似性,我们尽量将相同的代码组织在一起,例如:
<html> <head> <title>Script Example</title> <-- Example of inefficient script positioning --> <script type="text/javascript" src="file1.js"></script> <script type="text/javascript" src="file2.js"></script> <script type="text/javascript" src="file3.js"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <p>Hello world!</p> </body> </html>
这些看起来比较规整的代码有一个明显的性能问题就是:在<head>部分加载了三个JavaScript文件。因为每个<script>标签阻塞了页面的解析过程,直到它完整地下载并运行了外部JavaScript代码之后,页面处理才能继续进行。用户必须忍受这种可以察觉的延迟。请记住,浏览器在遇到<body>标签之前,不会渲染页面的任何部分。用这种方法把脚本放在页面的顶端,将导致一个可以察觉的延迟,通常表现为:页面打开时,首先显示为一幅空白的页面,而此时用户即不能阅读,也不能与页面进行交互操作。为了更好地理解此过程,我们使用瀑布图来描绘每个资源的下载过程。图1-1显示出页面加载过程中,每个脚本文件和样式表文件下载的过程。
第一个JavaScript文件开始下载,并阻塞了其他文件的下载过程。进一步,在file1.js下载完之后和file2.js开始下载之前有一个延时,这是file1.js完全运行所需的时间。每个
文件必须等待前一个文件下载完成并运行完之后,才能开始自己的下载过程。当这些文件下载时,用户面对一个空白的屏幕。这就是今天大多数浏览器的行为模式。Internet Explorer 8, Firefox 3.5, Safari 4, 和Chrome 2允许并行下载JavaScript文件。这个好消息表明,当一个<script>标签正在下载外部资源时,不必阻塞其他<script>标签。不幸的是,JavaScript的下载仍然要阻塞其他资源的下载过程,例如图片。即使脚本之间的下载过程互不阻塞,页面仍旧要等待所有JavaScript代码下载并执行完成之后才能继续。所以,当浏览器通过允许并行下载提高性能之后,该问题并没有完全解决。因为脚本阻塞其他页面资源的下载过程,所以推荐的办法是:将所有<script>标签放在尽可能接近<body>标签底部的位置,尽量减少对整个页面下载的影响。例如
<html> <head> <title>Script Example</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <p>Hello world!</p> <-- Example of recommended script positioning --> <script type="text/javascript" src="file1.js"></script> <script type="text/javascript" src="file2.js"></script> <script type="text/javascript" src="file3.js"></script> </body> </html>
此代码展示了所推荐的<script>标签在HTML文件中的位置。尽管脚本下载之间互相阻塞,但页面已经下载完成并且显示在用户面前了,进入页面的速度不会显得太慢。这正是“Yahoo! 优越性能小组”关于JavaScript的第一条定律:将脚本放在底部。
另外,浏览器请求4个25K的外部JS文件的速度要慢于请求1个100K的js文件,因此,我们应该尽量将JS文件包含在一个中加载。(每个HTTP请求都会产生额外的性能负担,下载
一个100KB的文件比下载四个25KB的文件要快)。
1.1 Nonblocking Scripts 非阻塞脚本
Deferred Scripts 延期脚本
HTML 4为<script>标签定义了一个扩展属性:defer。这个defer属性指明元素中所包含的脚本不打算修改DOM,因此代码可以稍后执行。defer属性只被Internet Explorer 4和Firefox 3.5更高版本的浏览器所支持,它不是一个理想的跨浏览器解决方案。在其他浏览器上,defer属性被忽略,<script>标签按照默认方式被处理(造成阻塞)。如果浏览器支持的话,这种方法仍是一种有用的解决方案。示例如下:
<script type="text/javascript" src="file1.js" defer></script>
一个带有defer属性的<script>标签可以放置在文档的任何位置。对应的JavaScript文件将在<script>被解时启动下载,但代码不会被执行,直到DOM加载完成(在onload事件句柄被调用之前)。当一个defer的JavaScript文件被下载时,它不会阻塞浏览器的其他处理过程,所以这些文件可以与页面的其他资源一起并行下载.(需要理解的是一个<script>脚本的执行时间包含两部分,(1)请求加载时间(2)JS执行时间。defer的作用是延迟JS的执行时间。任何带有defer属性的<script>元素在DOM加载完成之前不会被执行,不论是内联脚本还是外部脚本文件,都是这样。下面的例子展示了defer属性如何影响脚本行为。
<html> <head> <title>Script Defer Example</title> </head> <body> <script defer> alert("defer"); </script> <script> alert("script"); </script> <script> window.onload = function(){ alert("load"); }; </script> </body> </html>
这些代码在页面处理过程中弹出三个对话框。如果浏览器不支持defer属性,那么弹出对话框的顺序是“defer”,“script”和“load”。如果浏览器支持defer属性,那么弹出对话框的顺序是“script”,“defer”和“load”。注意,标记为defer的<script>元素不是跟在第二个后面运行,而是在onload事件句柄处理之前被调用。
Dynamic Script Elements 动态脚本元素
文档对象模型(DOM)允许你使用JavaScript动态创建HTML的几乎全部文档内容。其根本在于,<script>元素与页面其他元素没有什么不同:引用变量可以通过DOM进行检索,可以从文档中移动、删除,也可以被创建。一个新的<script>元素可以非常容易地通过标准DOM函数创建
var script = document.createElement ("script"); script.type = "text/javascript"; script.src = "file1.js"; document.getElementsByTagName_r("head")[0].appendChild(script);
新的<script>元素加载file1.js源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。
你甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的HTTP连接)
大多数情况下,你希望调用一个函数就可以实现JavaScript文件的动态加载。下面的函数封装了标准实现和IE实现所需的功能:
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 { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName_r("head")[0].appendChild(script); }
1.2 XMLHttpRequest Script Injection XHR脚本注入
另一个以非阻塞方式获得脚本的方法是使用XMLHttpRequest(XHR)对象将脚本注入到页面中。此技术首先创建一个XHR对象,然后下载JavaScript文件,接着用一个动态<script>元素将JavaScript代码注入页面。下面是一个简单的例子:
var xhr = new XMLHttpRequest(); xhr.open("get", "file1.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.responseText; document.body.appendChild(script); } } }; xhr.send(null);
此代码向服务器发送一个获取file1.js文件的GET请求。onreadystatechange事件处理函数检查readyState是不是4,然后检查HTTP状态码是不是有效(2XX表示有效的回应,304表示一个缓存响应)。如果收到了一个有效的响应,那么就创建一个新的<script>元素,将它的文本属性设置为从服务器接收到的responseText字符串。这样做实际上会创建一个带有内联代码的<script>元素。一旦新<script>元素被添加到文档,代码将被执行,并准备使用。
这种方法的主要优点是,你可以下载不立即执行的JavaScript代码。由于代码返回在<script>标签之外(换句话说不受<script>标签约束),它下载后不会自动执行,这使得你可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。此方法最主要的限制是:JavaScript文件必须与页面放置在同一个域内,不能从CDNs下载(CDN指“内容投递网络(Content Delivery Network)”,前面002篇《成组脚本》一节提到)。正因为这个原因,大型网页通常不采用XHR脚本注入技术。
1.3 Recommended Nonblocking Pattern 推荐的非阻塞模式
推荐的向页面加载大量JavaScript的方法分为两个步骤:第一步,包含动态加载JavaScript所需的代码,然后加载页面初始化所需的除JavaScript之外的部分。这部分代码尽量小,可能只包含loadScript()函数,它下载和运行非常迅速,不会对页面造成很大干扰。当初始代码准备好之后,用它来加载其余的JavaScript。
例如:
<script type="text/javascript" src="loader.js"></script> <script type="text/javascript"> loadScript("the-rest.js", function(){ Application.init(); }); </script>
将此代码放置在body的关闭标签</body>之前。这样做有几点好处:首先,像前面讨论过的那样,这样做确保JavaScript运行不会影响页面其他部分显示。其次,当第二部分JavaScript文件完成下载,所有应用程序所必须的DOM已经创建好了,并做好被访问的准备,避免使用额外的事件处理(例如window.onload)来得知页面是否已经准备好了
1.4 The LazyLoad library && The LABjs library
作为一个更通用的工具,Yahoo! Search的Ryan Grove创建了LazyLoad库(参见http://github.com/rgrove/lazyload/)。LazyLoad是一个更强大的loadScript()函数。LazyLoad精缩之后只有大约1.5KB(精缩,而不是用gzip压缩的)。具体用法不再赘述,请参考详细文档说明。
另一个非阻塞JavaScript加载库是LABjs(http://labjs.com/),Kyle Simpson写的一个开源库,由Steve Souders赞助。此库对加载过程进行更精细的控制,并尝试并行下载尽可能多的代码。LABjs也相当小,只有4.50KB(精缩,而不是用gzip压缩的),所以具有最小的页面代码尺寸
Summary
管理浏览器中的JavaScript代码是个棘手的问题,因为代码执行阻塞了其他浏览器处理过程,诸如用界面绘制。每次遇到<script>标签,页面必须停下来等待代码下载(如果是外部的)并执行,然后再继续处理页面其他部分。但是,有几种方法可以减少JavaScript对性能的影响:
[1] 将所有<script>标签放置在页面的底部,紧靠body关闭标签</body>的上方。此法可以保证页面在脚本运行之前完成解析。
[2] 将脚本成组打包。页面的<script>标签越少,页面的加载速度就越快,响应也更加迅速。不论外部脚本文件还是内联代码都是如此.
[3] 有几种方法可以使用非阻塞方式下载JavaScript:
——为<script>标签添加defer属性(只适用于Internet Explorer和Firefox 3.5以上版本)
——动态创建<script>元素,用它下载并执行代码
——用XHR对象下载代码,并注入到页面中