JavaScript 提高加载和执行效率的方法
本文深入解析 JavaScript 脚本对页面渲染的阻塞机制,探讨脚本放置位置、合并压缩及无阻塞加载技术。通过动态创建 script 节点、利用 defer 和 async 属性,以及 XHR 注入等手段,有效减少页面白屏时间,提升 Web 应用性能。
前言
JavaScript 脚本阻塞机制
无论 JavaScript 代码是内嵌在 HTML 中还是通过外链文件引入,浏览器的下载和渲染进程都必须暂停,直到脚本执行完毕。脚本执行耗时越长,浏览器响应用户交互的延迟就越高。造成这一阻塞的根本原因在于,脚本可能修改页面 DOM 结构或 JavaScript 命名空间,从而对后续页面内容的解析产生影响。
一个典型的例子就是在页面中使用document.write() 。
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 属性加载外链脚本时同样如此,浏览器需先下载外链文件,再解析执行,此期间页面渲染和用户交互完全被阻塞。
脚本放置位置优化
脚本位置
HTML 4 规范允许 <script> 标签置于 <head> 或 <body> 中,且可出现多次。Web 开发人员通常习惯在 <head> 中加载外链 JavaScript,并配合 <link> 标签加载 CSS 或其他页面资源。
低效率脚本位置示例
<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 示例所示,当浏览器解析到第 4 行的 <script> 标签时,会停止解析后续内容,优先下载并执行脚本。这意味着 styles.css 样式文件和 <body> 标签均无法加载,导致页面无法渲染,在 JavaScript 代码执行完毕前,页面呈现空白状态。
由于脚本会阻塞其他资源下载,建议将所有 <script> 标签尽可能放置在 <body> 标签底部,以最小化对页面整体下载的影响。
推荐的代码放置位置示例
<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>
该示例展示了 <script> 标签在 HTML 文档中的推荐位置。尽管脚本下载会阻塞其他脚本,但页面主体内容已下载并展示给用户,因此页面加载速度不会显得过慢。这是优化 JavaScript 的首要原则:将脚本置于页面底部。
脚本组织与合并
组织脚本
由于每个 <script> 标签在初始下载时都会阻塞页面渲染,减少页面中 <script> 标签的数量有助于改善这一状况。这不仅适用于外链脚本,内嵌脚本的数量也需限制。浏览器每遇到一个 <script> 标签,执行脚本都会带来一定延时,最小化这一延时将显著改善页面总体性能。
处理外链 JavaScript 文件时,需考虑 HTTP 请求带来的额外性能开销。下载单个 100Kb 的文件比下载 5 个 20Kb 的文件更快,即减少页面中外链脚本的数量有助于提升性能。
大型网站或应用通常依赖多个 JavaScript 文件。可将多个文件合并为一个,仅需引用一个 <script> 标签即可减少性能消耗。文件合并工作可通过离线打包工具或实时在线服务完成。
需特别注意,将内嵌脚本置于引用外链样式表的 <link> 标签之后,会导致页面阻塞以等待样式表下载,这是为了确保内嵌脚本执行时能获取精确的样式信息。因此,建议避免将内嵌脚本紧跟在 <link> 标签后。
无阻塞脚本加载技术
无阻塞的脚本
在功能丰富的 Web 应用或大型网站上,仅减少 JavaScript 文件大小和限制 HTTP 请求数往往不可行。Web 应用功能越丰富,所需 JavaScript 代码越多。尽管下载单个较大的 JavaScript 文件仅产生一次 HTTP 请求,却会占用浏览器大量时间。为避免此情况,需通过特定技术逐步加载 JavaScript 文件,从而在一定程度上避免阻塞浏览器。
无阻塞脚本的核心在于页面加载完成后再加载 JavaScript 代码,即在 window 对象的 onload 事件触发后再下载脚本。实现这一效果有多种方式。
defer 属性延迟加载
延迟加载脚本
HTML 4 为 <script> 标签定义了 defer 扩展属性。Defer 属性表明脚本不会修改 DOM,因此代码可安全延迟执行。该属性仅被 IE 4 和 Firefox 3.5 及以上版本支持,故非理想的跨浏览器解决方案。在不支持该属性的浏览器中,defer 属性会被忽略,<script> 标签按默认方式处理,即造成阻塞。但若目标浏览器支持,这仍是一个有用的解决方案。
defer 属性使用方法示例
<script type="text/javascript" src="script1.js" defer></script>
带有 defer 属性的 <script> 标签可置于文档任意位置。对应的 JavaScript 文件在页面解析到该标签时开始下载,但不执行,直到 DOM 加载完成(即 onload 事件触发前)才执行。带有 defer 属性的 JavaScript 文件下载时不阻塞浏览器其他进程,因此可与其他资源并行下载。
任何带有 defer 属性的 <script> 元素在 DOM 完成加载前均不执行,无论内嵌或外链脚本皆然。清单 5 示例展示了 defer 属性如何影响脚本行为:
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 脚本确实有用。若需支持跨领域的多种浏览器,则需更一致的实现方式。
async 属性异步加载
HTML 5 为 <script> 标签定义了新的扩展属性:async。其作用与 defer 类似,能异步加载和执行脚本,不阻塞页面加载。但需注意,有 async 属性时,JavaScript 脚本一旦下载完成即执行,因此可能不按原定顺序执行。若 JavaScript 脚本存在前后依赖,使用 async 可能导致错误。
动态脚本元素加载
动态脚本元素
文档对象模型(DOM)允许通过 JavaScript 动态创建 HTML 的几乎全部内容。<script> 元素与其他页面元素一样,可通过标准 DOM 函数轻松创建:
通过标准 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、Chrome 和 Safari 3+ 会在 <script> 节点接收完成后发出 onload 事件。可监听该事件以获取脚本准备好的通知:
通过监听 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 有五种取值:
微软文档指出,在 <script> 元素的生命周期中,readyState 的这些取值不一定全部出现,但未指明哪些取值总会被用到。实践中,我们最感兴趣的是“loaded”和“complete”状态。Internet Explorer 对这两个 readyState 值所表示的最终状态并不一致,有时 <script> 元素会得到“loader”却从不出现“complete”,但另一些情况下出现“complete”而用不到“loaded”。最安全的办法是在 readystatechange 事件中检查这两种状态,并在其中一种状态出现时,删除 readystatechange 事件句柄(保证事件不会被处理两次):
通过检查 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 实现所需的功能:
通过函数进行封装
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() 函数使用方法如下:
loadScript()函数使用方法
loadScript("script1.js", function(){
alert("File is loaded!");
});
您可以在页面中动态加载多个 JavaScript 文件,但需注意,浏览器不保证文件加载的顺序。所有主流浏览器中,仅 Firefox 和 Opera 保证脚本按指定顺序执行。其他浏览器将按照服务器返回次序下载并运行不同代码文件。可将下载操作串联起来以保证次序,如下:
通过 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 是一个简单示例:
通过 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 脚本注入技术。
总结
减少 JavaScript 对性能的影响有以下几种方法:
通过以上策略,可在很大程度上提高那些需要使用大量 JavaScript 的 Web 网站和应用的实际性能。
补充js加载函数:
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');
});
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
优化JS回到顶部性能:防抖节流与requestAnimationFrame应用
2026-11-05 16:33:51
JS中Array、Set与Map的区别及应用场景选择
2026-11-05 16:03:02
PixiJS 常见报错排查:纹理跨域、黑屏与内存泄漏解决
2026-11-05 10:05:21
JS数组find与filter取值区别及性能对比
2026-11-05 08:39:21
通过添加时间戳参数强制刷新JS资源
2026-11-05 08:32:36








