JavaScript异步加载策略与实现详解

作者:缓存笔记

时间:2026-10-28

浏览:127

本文深入解析JavaScript脚本加载机制,对比同步、defer与async属性的执行差异。通过代码示例演示如何在head或body中放置脚本以避免白屏,并详细讲解利用DOM动态插入script标签实现异步加载的方法,涵盖IE兼容处理及回调机制,旨在帮助开发者优化页面渲染性能与脚本依赖管理。

文章目录

JavaScript异步加载策略与实现详解

前言

在Web开发中,JavaScript脚本的加载问题屡见不鲜。主要涉及两个核心方面:

1. 同步与异步脚本对文件加载、依赖关系及执行顺序的影响。 2. 同步与异步脚本对页面性能优化的作用。

深入理解脚本加载的各个环节,不仅有助于解决实际问题,还能更好地把握并执行性能优化策略。 首先,观察一个普通的script标签代码——


代码如下:

<script src="js/myApp.js"></script>

若将上述代码置于<head>标签内,会阻塞所有页面渲染工作,导致用户在脚本加载并执行完毕前一直面对“白屏”状态。而将脚本放在<body>末尾,虽然避免了白屏,但用户看到的只是静态页面,原本应由客户端渲染的部分却显示为无作用的控件或空白区域。以下是一个测试用例——

代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>异步加载script</title>
<script src="js/test.js"></script>
</head>
<body>
<div>我是内容</div>
<img src="img/test.jpg">
</body>
</html>

其中,test.js的内容如下——


代码如下:

alert('我是head里面的脚本代码,执行这里的js之后,才开始进行body的内容渲染!');

运行上述代码时,由于alert是暂停点,页面会呈现空白。需要注意的是,此时整个页面实际上已经加载完毕,如果body中包含带有src属性的标签(如img标签),浏览器已经开始加载相关内容。关键点在于:JS引擎与渲染引擎的工作时机是互斥的(部分书籍称之为UI线程)。

因此,我们需要区分脚本的加载时机:那些提升页面视觉效果和交互体验的脚本应立即加载,而那些非关键的脚本可以稍后加载。

脚本延迟执行(defer)

一、脚本延迟执行(defer)

目前,将脚本放置在页面<body>标签的尾部已成为一种流行做法。这样做一方面让用户能更快看到页面内容,另一方面脚本可以直接操作已加载完成的DOM元素。对于大多数脚本而言,这种“搬家”带来了显著进步。该页面模型如下——


代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
<!--metadata and scriptsheets go here-->
<script src="headScript.js"></script>
</head>
<body>
<!--content goes here-->
<script src="bodyScript.js"></script>
</body>
</html>

这确实大大加快了页面的渲染时间,但需注意,这可能让用户在bodyScript加载完成前与页面交互。由于浏览器在加载完整个文档之前无法加载这些脚本,对于通过慢速连接传输的大型文档,这将成为一大瓶颈。

理想情况下,脚本的加载应与文档的加载同时进行,且不影响DOM渲染。这样,一旦文档就绪,脚本即可按照<script>标签的顺序运行。

我们可以使用defer属性来满足这一需求,即——


代码如下:

<script src="deferredScript.js"></script>

添加defer属性相当于告诉浏览器:请立即开始加载该脚本,但请等到文档就绪且此前所有具有defer属性的脚本执行完毕后再运行它。

这样,在head标签中放入延迟脚本,既能获得将脚本置于body标签时的所有好处,又能大幅提升大文档的加载速度。此时的页面模式如下——


代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
<!--metadata and scriptsheets go here-->
<script src="headScript.js"></script>
<script src="deferredScript.js" defer></script>
</head>
<body>
<!--content goes here-->
</body>
</html>

然而,并非所有浏览器都支持defer(对于部分现代浏览器,若声明defer,其内部脚本将不会执行document.write及DOM渲染操作。IE4及以上版本均支持defer属性)。这意味着,若想确保延迟脚本在文档加载后运行,必须将所有延迟脚本的代码封装在诸如jQuery的$(document).ready结构中。这是值得的,因为约97%的访客能享受并行加载的好处,而剩余3%的访客仍能使用功能完整的JavaScript。

脚本的完全并行化(async)

二、脚本的完全并行化(async)

让脚本的加载及执行再快一步,我不希望等到defer脚本一个接一个运行(defer让我们想到一种静静等待文档加载的有序排队场景),更不希望等到文档就绪后才运行这些脚本,我希望尽快加载并尽快运行这些脚本。这里便引入了HTML5的async属性,但需注意,它可能导致无序执行。

例如,我们加载两个完全不相干的第三方脚本,页面没有它们也能运行,且不在乎它们谁先运行。因此,对这些第三方脚本使用async属性,相当于免费提升了它们的运行速度。

async属性是HTML5新增的。作用和defer类似,即允许在下载脚本的同时进行DOM渲染。但它将在下载后尽快执行(即JS引擎空闲时立即执行),不能保证脚本会按顺序执行。它们将在onload事件之前完成。

Firefox 3.6、Opera 10.5、IE 9以及最新的Chrome和Safari都支持async属性。可以同时使用async和defer,这样IE4之后的所有IE都支持异步加载,但需注意,async会覆盖defer。

此时的页面模型如下——

代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
<!--metadata and scriptsheets go here-->
<script src="headScript.js"></script>
<script src="deferredScript.js" defer></script>
</head>
<body>
<!--content goes here-->
<script src="asyncScript1.js" async defer></script>
<script src="asyncScript2.js" async defer></script>
</body>
</html>

需注意这里的执行顺序:各脚本文件加载,接着执行headScript.js,紧接着在DOM渲染的同时会在后台加载deferredScript.js。在DOM渲染结束时,将运行deferredScript.js和那两个异步脚本。对于支持async属性的浏览器而言,这两个脚本将无序运行。

可编程的脚本加载

三、可编程的脚本加载

尽管上述两个脚本属性的功能非常吸引人,但由于兼容性问题,应用并不广泛。因此,我们更多使用脚本来加载其他脚本,例如,我们只想给满足特定条件的用户加载某个脚本,即常说的“懒加载”。

在浏览器API层面,有两种合理的方法来抓取并运行服务器脚本——

1. 生成ajax请求并用eval函数处理响应

2. 向DOM插入<script>标签

后一种方式更好,因为浏览器会替我们处理生成HTTP请求等事宜。此外,eval存在一些实际问题:泄露作用域、调试困难,且可能降低性能。因此,若想加载名为feature.js的脚本,应使用类似下面的代码:


代码如下:

var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = 'feature.js';
head.appendChild(script);

当然,我们需要处理回调监听。HTML5规范定义了一个可以绑定回调的onload属性。


代码如下:

script.onload = function() {
console.log('script loaded ...');
}

不过,IE8及更老版本并不支持onload,它们支持的是onreadystatechange。而且,对于错误处理仍然千奇百怪。在此,可多参考一些流行的脚本加载库,如labjs、yepnope、requirejs等。

如下,自己封装了一个简易loadjs文件——


代码如下:

var loadJS = function(url,callback){
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = url;
script.type = "text/javascript";
head.appendChild( script);
// script 标签,IE下有onreadystatechange事件, w3c标准有onload事件
 // IE9+也支持 W3C标准的onload
 var ua = navigator.userAgent,
 ua_version;
 // IE6/7/8
 if (/MSIE ([^;]+)/.test(ua)) {
 ua_version = parseFloat(RegExp["$1"], 10);
 if (ua_version <= 8) {
 script.onreadystatechange = function(){
 if (this.readyState == "loaded" ){
 callback();
 }
 }
 } else {
 script.onload = function(){
 callback();
 };
 }
 } else {
 script.onload = function(){
 callback();
 };
 }
};

对于document.write的方式异步加载脚本,在此不再赘述,因为现在很少有人这么做,浏览器差异性确实令人头疼。

需注意,使用Image对象异步预加载js文件,其中的js代码将不会被执行。

最后,谈一下requirejs中的异步加载脚本。

requirejs不会保证按顺序运行目标脚本,只是保证它们的运行次序能满足各自的依赖性要求。从而我们确保了尽快并行加载所有脚本,并有条不紊地按照依赖性拓扑结构执行这些脚本。

 

总结

四、总结

OK,谈到这儿,异步加载脚本的陈述也就完了。我再次啰嗦一下这里的优化顺序——

1. 传统方式,我们使用script标签直接嵌入到html文档中,这里分两种情况——

a. 嵌入到head标签中——需注意,这样做并不会影响文档内容中其他静态资源文件的并行加载,它影响的是文档内容的渲染,即此时的DOM渲染就会被阻塞,呈现白屏。

b. 嵌入到body标签底部——为了免去白屏现象,我们优先进行DOM渲染,再去执行脚本,但问题又来了。先说第一个问题——如果DOM文档内容比较大,交互事件绑定便有了延迟,体验便差了些。当然,我们需要根据需求而定,让重要的脚本优先执行。再说第二个问题——由于脚本文件至于body底部,导致对于这些脚本的加载相对于至于head中的脚本而言,它们的加载便有了延迟。所以,至于body底部,也并非是优化的终点。

c. 添加defer属性——我们希望脚本尽早进行并行加载,我们把这批脚本依旧放入head中。脚本的加载应该与文档的加载同时进行,并且不影响DOM的渲染。这样,一旦文档就绪就可以运行脚本。所以便有了defer这样属性。但是要注意它的兼容性,对于不支持defer属性的浏览器,我们需要将代码封装在诸如jQuery的$(document).ready中。需注意一点,所有的defer属性的脚本,是按照其出场顺序依次执行,因此,它同样严格同步。

2. 上一点,讲的都是同步执行脚本(需注意,这些脚本的加载过程是并行的,只不过是谁先触发请求谁后触发请求的区别而已),接下来的优化点便是“并行执行脚本”,当然,我们知道,一个时间点,只有执行一个js文件,这里的“并行”是指,谁先加载完了,只要此时js引擎空闲,立马执行之。这里的优化分成两种——

a. 添加async这个属性——确实能够完成上面我们所说的优化点,但是它有很高的局限性,即仅仅是针对非依赖性脚本加载,最恰当的例子便是引入多个第三方脚本了。还有就是与defer属性的合用,实在是让人大费脑筋。当然,它也存在兼容性问题。以上三个问题便导致其应用并不广泛。当使用async的时候,一定要严格注意依赖性问题。

b. 脚本加载脚本——很显然,我们使用之来达到“并行执行脚本”的目的。同时,我们也方便去控制脚本依赖的问题,我们便使用了如requirejs中对于js异步加载的智能化加载管理。

好,写到这儿。

这里,我仅仅谈的是异步加载脚本的相关内容。还有一块儿内容,便是异步加载样式文件或者其他静态资源。待续......

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章