本文深入探讨JavaScript加载与执行对页面性能的影响,指出脚本阻塞渲染的根本原因。通过推荐将脚本置于页面底部、合并脚本文件以减少HTTP请求,以及采用defer、async、动态脚本节点和XHR注入等无阻塞加载技术,有效解决脚本执行导致的页面白屏与交互延迟问题,提升Web应用的实际性能。
JavaScript性能优化:脚本加载与执行策略详解
前言
不论 JavaScript 代码是内嵌还是外链,页面下载与渲染均需等待脚本执行完毕。脚本执行耗时越长,浏览器响应用户输入延迟越久。浏览器在下载及执行脚本时出现阻塞,是因为脚本可能改变页面或 JavaScript 命名空间,进而影响后续页面内容。典型示例为在页面中使用document.write(),参见清单 1
清单 1 JavaScript 代码内嵌示例
<html>
<head>
<title>Source Example</title>
</head>
<body>
<p>
<script type="text/javascript">
document.write("Today is " + (new Date()).toDateString());
</script>
</p>
</body>
</html>
浏览器遇到<script>标签时,无法预知 JavaScript 是否向<p>标签添加内容、引入其他元素或移除该标签。因此,浏览器会停止处理页面,优先执行 JavaScript 代码,随后继续解析和渲染页面。使用 src 属性加载 JavaScript 时同理,浏览器需先下载外链文件代码,再解析执行。此过程中,页面渲染和用户交互完全被阻塞。
脚本位置对性能的影响
脚本位置
HTML 4 规范规定<script>标签可置于 HTML 文档的<head>或<body>中,且可多次出现。Web 开发人员通常习惯在<head>中加载外链 JavaScript,并使用<link>标签加载外链 CSS 文件或其他页面信息。例如清单 2
清单 2 低效率脚本位置示例
<html>
<head>
<title>Source Example</title>
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
</body>
</html>
然而这种常规做法存在严重性能问题。如清单 2 所示,当浏览器解析到<script>标签(第 4 行)时,会停止解析后续内容,优先下载并执行脚本文件。这意味着其后的 styles.css 样式文件和<body>标签均无法加载。由于<body>未加载,页面无法渲染,导致在该 JavaScript 代码完全执行前,页面显示为空白。图 1 展示了页面加载过程中脚本和样式文件的下载情况。
图 1 JavaScript 文件的加载和执行阻塞其他文件的下载

观察发现一个有趣现象:第一个 JavaScript 文件开始下载时,同时阻塞了页面其他文件的下载。此外,从 script1.JS 下载完成到 script2.js 开始下载前存在延时,这段时间正是 script1.js 的执行过程。每个文件必须等待前一个文件下载并执行完成后才能开始下载。在此逐个下载过程中,用户看到的是空白页面。
自 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 起,均允许并行下载 JavaScript 文件。这是一个好消息,因为<script>标签在下载外部资源时不会阻塞其他<script>标签。遗憾的是,JavaScript 下载过程仍会阻塞其他资源(如样式文件和图片)的下载。尽管脚本下载互不影响,但页面仍需等待所有 JavaScript 代码下载并执行完成才能继续。因此,尽管最新浏览器通过允许并行下载提升了性能,但问题未完全解决,脚本阻塞依然存在。
鉴于脚本会阻塞页面其他资源下载,推荐将所有<script>标签尽可能置于<body>标签底部,以最小化对整体页面下载的影响。例如清单 3
清单 3 推荐的代码放置位置示例
<html>
<head>
<title>Source Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
<!-- Example of efficient script positioning -->
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
</body>
</html>
该代码展示了 HTML 文档中放置<script>标签的推荐位置。尽管脚本下载会阻塞另一个脚本,但页面大部分内容已下载并展示给用户,因此页面下载速度不会显得过慢。这是优化 JavaScript 的首要规则:将脚本放在底部。
组织脚本以减少阻塞
组织脚本
由于每个<script>标签初始下载时都会阻塞页面渲染,减少页面包含的<script>标签数量有助于改善这一情况。这不仅针对外链脚本,内嵌脚本数量同样需限制。浏览器解析 HTML 页面时,每遇到一个<script>标签,都会因执行脚本导致一定延时,因此最小化延迟时间将显著改善页面总体性能。
处理外链 JavaScript 文件时情况略有不同。考虑到 HTTP 请求会带来额外性能开销,下载单个 100Kb 的文件比下载 5 个 20Kb 的文件更快。即,减少页面中外链脚本数量将改善性能。
通常大型网站或应用需依赖数个 JavaScript 文件。可将多个文件合并为一个,只需引用一个<script>标签,即可减少性能消耗。文件合并工作可通过离线打包工具或实时在线服务实现。
需特别提醒的是,将内嵌脚本置于引用外链样式表的<link>之后,会导致页面阻塞以等待样式表下载。此举旨在确保内嵌脚本执行时能获得最精确的样式信息。因此,建议不要把内嵌脚本紧跟在<link>标签后面。
实现无阻塞脚本加载
无阻塞的脚本
减少 JavaScript 文件大小并限制 HTTP 请求数在功能丰富的 Web 应用或大型网站上并不总是可行。Web 应用功能越丰富,所需 JavaScript 代码越多,尽管下载单个较大的 JavaScript 文件只产生一次 HTTP 请求,却会锁死浏览器一大段时间。为避免这种情况,需通过特定技术向页面逐步加载 JavaScript 文件,从而在某种程度上避免阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载 JavaScript 代码。这意味着在 window 对象的 onload 事件触发后再下载脚本。有多种方式可实现这一效果。
使用defer属性延迟执行
延迟加载脚本
HTML 4 为<script>标签定义了一个扩展属性:defer。Defer 属性指明本元素所含脚本不会修改 DOM,因此代码能安全地延迟执行。defer 属性仅被 IE 4 和 Firefox 3.5 更高版本的浏览器所支持,故其并非理想的跨浏览器解决方案。在其他浏览器中,defer 属性会被直接忽略,因此<script>标签会以默认方式处理,即造成阻塞。然而,若目标浏览器支持,这仍是种有用的解决方案。清单 4 为示例
清单 4 defer 属性使用方法示例
<script type="text/javascript" src="script1.js" defer></script>
带有 defer 属性的<script>标签可置于文档任何位置。对应的 JavaScript 文件将在页面解析到<script>标签时开始下载,但不会执行,直到 DOM 加载完成,即 onload 事件触发前才会被执行。当一个带有 defer 属性的 JavaScript 文件下载时,它不会阻塞浏览器其他进程,因此这类文件可与其他资源文件并行下载。
任何带有 defer 属性的<script>元素在 DOM 完成加载前都不会被执行,无论内嵌或外链脚本皆如此。清单 5 示例展示了 defer 属性如何影响脚本行为:
清单 5 defer 属性对脚本行为的影响
<html>
<head>
<title>Script Defer Example</title>
</head>
<body>
<script type="text/javascript" defer>
alert("defer");
</script>
<script type="text/javascript">
alert("script");
</script>
<script type="text/javascript">
window.onload = function(){
alert("load");
};
</script>
</body>
</html>
该代码在页面处理过程中弹出三次对话框。不支持 defer 属性的浏览器弹出顺序为:“defer”、“script”、“load”。而在支持 defer 属性的浏览器上,弹出顺序则为:“script”、“defer”、“load”。请注意,带有 defer 属性的<script>元素并非跟在第二个后面执行,而是在 onload 事件被触发前被调用。
若目标浏览器仅包括 Internet Explorer 和 Firefox 3.5,则 defer 脚本确有用处。若需支持跨领域的多种浏览器,则还有更一致的实现方式。
HTML 5 为<script>标签定义了一个新的扩展属性:async。其作用与 defer 相同,能够异步加载和执行脚本,不因为加载脚本而阻塞页面加载。但需注意一点,在有 async 的情况下,JavaScript 脚本一旦下载好就会执行,所以极有可能不是按照原本顺序来执行。若 JavaScript 脚本前后有依赖性,使用 async 就极有可能出现错误。
动态创建脚本元素
动态脚本元素
文档对象模型(DOM)允许您使用 JavaScript 动态创建 HTML 的几乎全部文档内容。<script>元素与页面其他元素一样,可以非常容易地通过标准 DOM 函数创建:
清单 6 通过标准 DOM 函数创建<script>元素
var script = document.createElement ("script");
script.type = "text/javascript";
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
新的<script>元素加载 script1.js 源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。您甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)。
当文件使用动态脚本节点下载时,返回的代码通常立即执行(除了 Firefox 和 Opera,他们将等待此前的所有动态脚本节点执行完毕)。当脚本是“自运行”类型时,这一机制运行正常,但是如果脚本只包含供页面其他脚本调用调用的接口,则会带来问题。这种情况下,您需要跟踪脚本下载完成并是否准备妥善。可以使用动态 <script> 节点发出事件得到相关信息。
Firefox、Opera, Chorme 和 Safari 3+会在<script>节点接收完成之后发出一个 onload 事件。您可以监听这一事件,以得到脚本准备好的通知:
清单 7 通过监听 onload 事件加载 JavaScript 脚本
var script = document.createElement ("script")
script.type = "text/javascript";
//Firefox, Opera, Chrome, Safari 3+
script.onload = function(){
alert("Script loaded!");
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
Internet Explorer 支持另一种实现方式,它发出一个 readystatechange 事件。<script>元素有一个 readyState 属性,它的值随着下载外部文件的过程而改变。readyState 有五种取值:
1、“uninitialized”:默认状态
2、“loading”:下载开始
3、“loaded”:下载完成
4、“interactive”:下载完成但尚不可用
5、“complete”:所有数据已经准备好
微软文档上说,在<script>元素的生命周期中,readyState 的这些取值不一定全部出现,但并没有指出哪些取值总会被用到。实践中,我们最感兴趣的是“loaded”和“complete”状态。Internet Explorer 对这两个 readyState 值所表示的最终状态并不一致,有时<script>元素会得到“loader”却从不出现“complete”,但另外一些情况下出现“complete”而用不到“loaded”。最安全的办法就是在 readystatechange 事件中检查这两种状态,并且当其中一种状态出现时,删除 readystatechange 事件句柄(保证事件不会被处理两次):
清单 8 通过检查 readyState 状态加载 JavaScript 脚本
var script = document.createElement("script")
script.type = "text/javascript";
//Internet Explorer
script.onreadystatechange = function(){
if (script.readyState == "loaded" || script.readyState == "complete"){
script.onreadystatechange = null;
alert("Script loaded.");
}
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);
大多数情况下,您希望调用一个函数就可以实现 JavaScript 文件的动态加载。下面的函数封装了标准实现和 IE 实现所需的功能:
清单 9 通过函数进行封装
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("head")[0].appendChild(script);
}
此函数接收两个参数:JavaScript 文件的 URL,和一个当 JavaScript 接收完成时触发的回调函数。属性检查用于决定监视哪种事件。最后一步,设置 src 属性,并将<script>元素添加至页面。此 loadScript() 函数使用方法如下:
清单 10 loadScript()函数使用方法
loadScript("script1.js", function(){
alert("File is loaded!");
});
您可以在页面中动态加载很多 JavaScript 文件,但要注意,浏览器不保证文件加载的顺序。所有主流浏览器之中,只有 Firefox 和 Opera 保证脚本按照您指定的顺序执行。其他浏览器将按照服务器返回它们的次序下载并运行不同的代码文件。您可以将下载操作串联在一起以保证他们的次序,如下:
清单 11 通过 loadScript()函数加载多个 JavaScript 脚本
loadScript("script1.js", function(){
loadScript("script2.js", function(){
loadScript("script3.js", function(){
alert("All files are loaded!");
});
});
});
此代码等待 script1.js 可用之后才开始加载 script2.js,等 script2.js 可用之后才开始加载 script3.js。虽然此方法可行,但如果要下载和执行的文件很多,还是有些麻烦。如果多个文件的次序十分重要,更好的办法是将这些文件按照正确的次序连接成一个文件。独立文件可以一次性下载所有代码(由于这是异步进行的,使用一个大文件并没有什么损失)。
动态脚本加载是非阻塞 JavaScript 下载中最常用的模式,因为它可以跨浏览器,而且简单易用。
利用XHR对象注入脚本
使用 XMLHttpRequest(XHR)对象
此技术首先创建一个 XHR 对象,然后下载 JavaScript 文件,接着用一个动态 <script> 元素将 JavaScript 代码注入页面。清单 12 是一个简单的例子:
清单 12 通过 XHR 对象加载 JavaScript 脚本
var xhr = new XMLHttpRequest();
xhr.open("get", "script1.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);
此代码向服务器发送一个获取 script1.js 文件的 GET 请求。onreadystatechange 事件处理函数检查 readyState 是不是 4,然后检查 HTTP 状态码是不是有效(2XX 表示有效的回应,304 表示一个缓存响应)。如果收到了一个有效的响应,那么就创建一个新的<script>元素,将它的文本属性设置为从服务器接收到的 responseText 字符串。这样做实际上会创建一个带有内联代码的<script>元素。一旦新<script>元素被添加到文档,代码将被执行,并准备使用。
这种方法的主要优点是,您可以下载不立即执行的 JavaScript 代码。由于代码返回在<script>标签之外(换句话说不受<script>标签约束),它下载后不会自动执行,这使得您可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。
此方法最主要的限制是:JavaScript 文件必须与页面放置在同一个域内,不能从 CDN 下载(CDN 指"内容投递网络(Content Delivery Network)",所以大型网页通常不采用 XHR 脚本注入技术。
补充几个平时自己在用的函数
function loadJs(url, callback, charset) {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
if ( !!charset) script.charset = "utf-8";
script.src = url;
script.onload = script.onreadystatechange = function() {
var f = script.readyState;
if (f && f != "loaded" && f != "complete") return;
script.onload = script.onreadystatechange = null;
head.removeChild(script) if (callback) {
callback() || callback
};
};
head.appendChild(script);
}
// js同步加载
function getScripts(i, linkArray, fn) {
env || getEnv();
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = linkArray[i];
var head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(script);
if (env.ie && 'onreadystatechange' in script && !('draggable' in script)){ //ie浏览器使用以下方式加载
script.onreadystatechange = function () {
if (/loaded|complete/.test(script.readyState)) {
script.onreadystatechange = null;
if(i === linkArray.length-1) {
if (fn) {
fn();
}
} else {
getScripts(++i, linkArray, fn);
}
}
};
}else{
script.onload = function() {
if(i === linkArray.length-1) {
if (fn) {
fn();
}
} else {
getScripts(++i, linkArray, fn);
}
};
}
}
// js存在依赖关系 依次加载
getScripts(0, [
'http://caibaojian.com/demo/base.js',
'http://caibaojian.com/demo/reset.js'], function() {
alert('callback');
});
总结
减少 JavaScript 对性能的影响有以下几种方法:
将所有的<script>标签放到页面底部,也就是</body>闭合标签之前,这能确保在脚本执行前页面已经完成了渲染。
尽可能地合并脚本。页面中的<script>标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。
采用无阻塞下载 JavaScript 脚本的方法:
使用<script>标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版本);
使用动态创建的<script>元素来下载并执行代码;
使用 XHR 对象下载 JavaScript 代码并注入页面中。
通过以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 网站和应用的实际性能。
以上就是JavaScript性能优化总结之加载与执行的全部内容,希望对大家有所帮助。