仿佛是与生俱来的 - JavaScript 一诞生就会出现很多的性能问题。语言本身的阻塞特性再加上浏览器使用了单一的进程处理用户界面(UI)刷新和 JavaScript 脚本执行,这导致同一时刻浏览器只会做一件事情,JavaScript 脚本执行过程耗时越长,浏览器等待响应的时间就越长。
众所周知,在 HTML 页面结构代码中我们是通过 <script> 标签引入 JavaScript 脚本的。
有形如
<script type="text/javascript" src="[url]"></script>
直接引入 JavaScript 脚本源文件地址
还有形如
<script type="text/javascript">
document.write("The date is " + new Date().toDateString());
</script>
直接内嵌 JavaScript 脚本
浏览器为了保证页面处理正常,当页面解析到 <script> 标签时,就会停止对页面的处理,先执行 JavaScript 脚本,等脚本执行完后,继续解析和渲染页面结构。看到这里,相信会有人问:浏览器为什么会这样处理,一溜执行下去不好吗?接下来上代码!!!
......
<body>
<p>
<script type="text/javascript">
document.write("The date is " + new Date().toDateString());
</script>
</p>
</body>
......
对于上面的这段代码,当浏览器解析到 <p> 标签这里,发现里面有个叫 <script> 的标签,浏览器是不知道这个叫 <script> 的标签它葫芦里卖得什么药的(是否会对已经解析过得标签进行修改)。为了搞清楚这只葫芦里面的问题,浏览器必须停下来一心一意的处理这个葫芦,等这块的 JavaScript 脚本执行,然后继续往下进行解析和渲染。这样一方面保证了整个页面的完整性和统一性,另一方面也保证了整个页面的顺利解析。
走到这里,我们不禁会问,这个脚本的加载和解析竟然会这么麻烦,怎样才能最大程度的减少解析脚本的耗时,从而进行最大程度的优化呢?
脚本位置
说到这个脚本放置的位置,曾几何时,我也经常纠结于将 <script> 标签放在什么地方?
根据 HTML4 规范给出 <script> 标签可以放在 <head> 或 <body> 标签里面,并允许出现多次。一般认为将作为 JavaScript 脚本引入的 <script> 标签和作为 CSS 样式文件引入的 <link> 标签同时放在 <head> 标签里面,形如:
<head>
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<link rel="stylesheet" type="text/css" href="file1.js"/>
</head>
由于 JavaScript 脚本的阻塞特性,浏览器解析上面代码是按由上到下的顺序解析的。当解析到 <script> 标签时,浏览器会停止页面的渲染,一心一意对每个 <script> 标签进行下载、解析,这样一来用户的体验效果就会收到很明显的影响,更有可能出现页面显示空白的现象。
为此,IE8/Firefox 3.5/Safari 4/Chrome 2 都允许 JavaScript 文件的并行下载,但是在 JavaScript 文件下载的过程中仍然会阻塞其他资源的下载。不得已,推荐将所有的 <script> 标签尽可能的放到 <body> 标签的底部,以便尽量减少对整个页面渲染的影响,形如:
......
<body>
<!-- other html tags -->
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
</body>
......
这样做的优点:1、因为浏览器解析、渲染页面是由上至下的顺序,让 JavaScript 脚本在 DOM 解析完之后下载并解析,保证了整体页面渲染的一致性和顺序性,避免了因 JavaScript 脚本中途解析导致页面显示得不正常和 DOM 节点获取不到的异常,2、避免了因 JavaScript 文件的阻塞特性,导致整体 HTML 渲染的性能问题。
组织脚本
由于每个 <script> 标签初始下载时都会阻塞页面渲染,所以减少页面包含的 <script> 标签数量有助于改善页面渲染性能问题。对于外链的 JavaScript 文件,HTTP 请求会存在额外的性能开销,因此下载一个 100KB 的文件比下载 4 个 25KB 的文件会更快。
接下来,通过各种无阻塞模式来优化脚本文件下载解析的性能问题。
延迟脚本
Defer 属性指明本元素所含有的脚本不会修改 DOM ,因此当浏览器解析到对应的 <script> 标签是开始下载 JavaScript 文件(此时的 JavaScript 文件的下载不会阻塞其他资源文件的下载),但并不会立即执行,直到 DOM 加载完成( onload 事件被触发前)。它不是一种跨浏览器的解决方案,因为该属性仅支持 IE 4+ 和 Firefox 3.5+ 的浏览器。
<body>
<script defer>
alert('defer');
</script>
<script>
alert('script');
</script>
<script>
window.onload = function(){
alert('load');
}
</script>
</body>
上面的代码,在支持 defer 属性的浏览器中依次弹出 'script', 'defer', 'load',在不支持 defer 属性的浏览器中依次弹出 'defer', 'script', 'load'。
动态脚本元素
动态脚本加载可能是现今比较流行的优化技术了。由于 JavaScript 脚本可以对 DOM 进行增删改查,那创建 <script> 标签进行脚本引入也就自然而然了。
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file1.js';
document.getElementsByTagName('head')[0].appendChild(script);
这段脚本创建了一个 <script> 标签来加载 file1.js 文件。文件在该元素添加到页面时开始下载,并立即执行返回的脚本文件(除了 Firefox 和 Opera,他们会等待所有所有动态脚本节点执行完毕)。为了实现跨浏览器,我们将上面的脚本优化如下
function loadScript(url){
var script = document.createElement('script');
script.type = 'text/javascript';
if(script.readyState){ //IE
//在 IE 浏览器中对 <script> 元素的接受会触发 readystatechange 事件
// <script> 元素提供了 readystate 属性,根据不同阶段,取值分别为
// uninitialized => 初始化状态
// loading => 开始下载
// loaded => 下载完成
// interactive => 数据完成下载但上不可用
// complete => 所有数据已准备就绪
script.onreadystatechange = function(){
if(script.readystate == 'load' || script.readystate == 'complete'){
script.onreadystatechange = null;
callback();
}
}
}else{ //其他浏览器
script.onload = function(){
callback()
}
}
script.src = 'file1.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
上面的方法调用脚本加载形式如下:
loadScript('file1.js', function(){
alert('file1 is loaded!');
loadScript('file2.js', function(){
alert('file2 is loaded!');
})
})
XMLHttpRequest 脚本注入
无阻塞脚本加载的方式是 XMLHttpRequest 脚本注入,此技术会创建一个 XHR 对象,然后用它下载 JavaScript 文件,最后通过创建动态的 <script> 元素将代码注入到页面当中。
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);
代码发送一个 GET 请求获取 file1.js 源文件。事件处理函数 onreadystatechange 检查 readystate 是否为 4 ,同时校验 HTTP 状态码是否有效( 2XX 表示有效响应,304 标识从缓存读取 )。如果收到有效响应,就立即创建 <script> 元素,并设置该元素的 text 属性为从服务器端接收的 responseText,创建一个带内联脚本的 <script> 标签。
这样做的好处是,可以自主的控制脚本执行的时机。由于脚本内容是在 <script> 标签之外的,因此下载后不会立即执行。还有就是同样的代码几乎所有的浏览器都能正常工作。这种方法主要的局限性是,JavaScript 文件必须和所请求的页面处于相同的域。
管理浏览器中的 JavaScript 脚本是一个棘手的问题,由于 JavaScript 阻塞特性,通过以上的策略可以极大的提高需要大量使用 JavaScript 的 web 应用的实际性能。