高性能的JavaScript--加载和执行
JavaScript在浏览器中的性能,可认为是开发者所要面对的最重要的可用性的问题,此问题因JavaScript的阻塞特征而复杂,也就是说JavaScript运行时其他的事情不能被浏览器处理,事实上,大多数浏览器使用单进程处理UI更新和JavaScript运行等多个任务,而同一时间只能有一个任务被执行。JavaScript运行了多长时间,那么浏览器空闲下来响应用户输入之前的等待时间就有多长。
从基本层面说,这就意味着<script>标签的出现使整个页面因脚本解析、运行出现等待。不论实际的JavaScript代码是内联的还是包含在一个不相干的外部文件中页面下载和解析过程必须停下,等待脚本完成这些处理,然后才能继续,也是页面生命周期必不可少的部分,因为脚本可能在运行过程中修改页面内容。
在加载JavaScript过程中,页面解析和用户交互是被完全阻塞的。
HTML 4 文档指出,一个<script>标签可以放在 HTML文档的 <head> 或者<body>标签中,可以在其中多次出现。传统上,<script> 标签用于加载外部JavaScript 文件。<head>部分除此类代码外,还包含 <link>标签用于加载外部css文件和其他页面中间件。也就是说,最好把风格和行为所依赖的部分放在一起,首先加载他们,使他们可以得到正确的外观和行为。
例如:
1 <html> 2 <head> 3 <title>Script Example</title> 4 <-- Example of ineffi cient script positioning --> 5 <script type="text/javascript" src="file1.js"></script> 6 <script type="text/javascript" src="file2.js"></script> 7 <script type="text/javascript" src="file3.js"></script> 8 <link rel="stylesheet" type="text/css" href="styles.css"> 9 </head> 10 <body> 11 <p>Hello world!</p> 12 </body> 13 </html>
虽然这些代码看起来没什么问题,但是在〈head〉部分加载了三个JavaScript文件。每个〈script〉标签阻塞了页面解析过程,直到完整的下载并运行了外部JavaScript代码之后,页面才能继续进行。在浏览器没有遇到〈body〉标签之前,不会渲染页面的任何部分。
把脚本放在页面的顶端,将会导致一个可以察觉的延迟,通常表现为:页面打开一片白,用户不能阅读和操作。
如图,当第一javas文件开始下载时,阻塞了其他文件下载。进一步当第一个文件下载完成之后和第二个文件下载之前有一个延时,是第一个文件完全运行所需要的时间。
解决这个问题推荐的办法是:将所有<script> 标签放在尽可能接近<body> 标签的底部位置,尽量减少对整个页面下载的影响。
如:
1 <html> 2 <head> 3 <title>Script Example</title> 4 <link rel="stylesheet" type="text/css" href="styles.css"> 5 </head> 6 <body> 7 <p>Hello world!</p> 8 <-- Example of recommended script positioning --> 9 <script type="text/javascript" src="file1.js"></script> 10 <script type="text/javascript" src="file2.js"></script> 11 <script type="text/javascript" src="file3.js"></script> 12 </body> 13 </html>
由于每个<script>标签下载时阻塞页面解析过程,所以限制页面的<script>总数也是可以改善性能。这个规则对内联脚本和外部脚本同样适用。每当页面解析碰到一个<script>标签时,紧接着有一段时间用于代码执行。最小化这些延迟时间可以改善页面的整体性能。
每个HTTP请求都会产生额外的性能负担,下载一个100KB的文件比下载4个25KB的文件要快。总之,减少引用外部文件的数量。典型的,一个大型网站或者网页应用需要多次请求JavaScript文件。你可以将这些文件整合成一个文件,只需要一个<script>标签引用,就可以减少性能损失。
JavaScript倾向于阻塞浏览器某些处理过程,如HTTP请求和界面刷新,这是开发者面临的最显著的性能问题。保持JavaScript文件短小,并限制HTTP请求的数量,只是创建反应迅速的网页应用的第一步。一个应用程序所包含的功能越多,所需要的JavaScript代码就越大,保持源码短小并不总是一种选择。尽可能下载一个大JavaScript文件只产生一次HTTP请求。却会锁住浏览器一大段时间。为避开这种情况,你需要向页面中逐步添加JavaScript,某种程度上说不会阻塞浏览器。
非阻塞脚本的秘密在于,等页面加载之后,再加载JavaScript源码。从技术角度上讲,这意味着在window的load事件发出之后下载代码。有几种方法可以实现这种效果。
1.延期脚本
HTML4为<script>标签定义了一个扩展属性:defer。这个defer属性指明元素中所包含的脚本不打算修改DOM,因此代码可以稍后执行(适用于IE4以上浏览器)
<script type="text/javascript" src="file1.js" defer></script>
带有该属性的JavaScript文件在<script>被解析时启动下载,但代码不会被执行,直到DOM加载完成,它不会阻塞浏览器的其他处理过程,所以这些文件可以与页面的其他资源一起并行下载。
2.动态脚本元素
文档对象模型dom允许使用JavaScript动态创建HTML的几乎全部文档内容。其根本在于<script>元素与页面其他元素没有什么不同。
当文件使用动态脚本节点下载时,返回的代码通常立即执行。当脚本“自运行”类型时这一机制运行正常,但是如果脚本只包含页面其他脚本调用的的接口,则会带来问题。这种情况下,你需要跟踪脚本下载完成并准备妥善的情况。
IE 会发出一个readystatechange事件。<script>元素有一个readyState属性,它的值随着外部下载的过程而改变。readyState有5种取值。
uninitialized 默认状态
loading 开始下载
interactive 下载完成但尚不可用
complete 所有数据都已经准备好
下面封装一个函数来实现JavaScript文件的动态加载:
1 function loadScript (url, callback){ 2 var script = document.createElement ("script") 3 script.type = "text/javascript"; 4 if (script.readyState){ //IE 5 script.onreadystatechange = function(){ 6 if (script.readyState == "loaded" || script.readyState == "complete"){ 7 script.onreadystatechange = null; 8 callback(); 9 } 10 }; 11 } else { //Others 12 script.onload = function(){ 13 callback(); 14 }; 15 } 16 script.src = url; 17 document.getElementsByTagName_r("head")[0].appendChild(script); 18 }
使用方法:
1 loadScript("file1.js", function(){ 2 alert("File is loaded!"); 3 });
使文件按顺序加载:
1 loadScript("file1.js", function(){ 2 loadScript("file2.js", function(){ 3 loadScript("file3.js", function(){ 4 alert("All files are loaded!"); 5 }); 6 }); 7 });
3.XHR脚本注入
使用XMLHttpRequest(XHR)对象将脚本注入到页面中。此技术首先创建一个XHR对象,然后下载javas文件,接着用一个动态<script>元素将javas代码注入页面。
1 var xhr = new XMLHttpRequest(); 2 xhr.open("get", "file1.js", true); 3 xhr.onreadystatechange = function(){ 4 if (xhr.readyState == 4){ 5 if (xhr.status >= 200 && xhr.status < 300 | | xhr.status == 304){ 6 var script = document.createElement ("script"); 7 script.type = "text/javascript"; 8 script.text = xhr.responseText; 9 document.body.appendChild(script); 10 } 11 } 12 }; 13 xhr.send(null);
此代码向服务器发送一个获取file1.js文件的GET请求。onreadystatechange事件处理函数检查readyState是不是4,然后检查HTTP状态码是不是有效(2XX表示有效回应,304表示一个缓存响应)。如果收到一个有效的响应,那么就创建一个新的<script>元素,将它的文本属性设置为从服务器接受到的resposeText字符串。这样做实际上会创建一个带有内联代码的<script>元素。一旦新的<script>元素被添加到文档,代码将被执行并准备使用。
这种方法的主要优点是,您可以下载不立即执行的 JavaScript 代码。由于代码返回在<script>
标签之外(换句话说不受<script>
标签约束),它下载后不会自动执行,这使得您可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。
此方法最主要的限制是:JavaScript 文件必须与页面放置在同一个域内,不能从 CDN 下载(CDN 指”内容投递网络(Content Delivery Network)”,所以大型网页通常不采用 XHR 脚本注入技术。
减少 JavaScript 对性能的影响有以下几种方法:
- 将所有的
<script>
标签放到页面底部,也就是</body>
闭合标签之前,这能确保在脚本执行前页面已经完成了渲染。 - 尽可能地合并脚本。页面中的
<script>
标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。 - 采用无阻塞下载 JavaScript 脚本的方法:
- 使用
<script>
标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版本); - 使用动态创建的
<script>
元素来下载并执行代码; - 使用 XHR 对象下载 JavaScript 代码并注入页面中。