JavaScript 异步加载机制:同步阻塞、async 与 defer 深度解析

作者:木棉

时间:2026-10-22

浏览:217

本文深入分析 JavaScript 默认同步加载导致的页面渲染阻塞问题,探讨动态插入 script 标签的局限性。重点对比 HTML5 中 async 与 defer 属性的执行机制,指出 async 乱序执行与 defer 按序延迟执行的区别,并结合 DOMContentLoaded 与 onload 事件,指导开发者根据依赖关系选择合适的异步加载策略。

文章目录

同步加载的问题

JavaScript 默认采用同步加载模式,此处的“加载”主要指代码的解析与执行过程,而非网络下载。在现代浏览器中,资源请求通常采用瀑布流方式并行下载,但 JavaScript 脚本的执行却是阻塞式的。这种机制会引发显著问题:若页面引用的某个 JS 文件请求响应缓慢,将阻塞后续所有 JS 代码的执行;同时,如果脚本标签位于 head 标签内,页面渲染也会随之停滞。


<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
this is a test

以 index.html 为例,页面主体仅包含简单字符串,但执行后页面长时间保持空白。原因在于引用的 JS 文件因网络原因(如被墙)无法及时加载,导致后续代码执行阻塞,页面渲染无法进行。有人建议将 JS 脚本移至 </body> 标签前以优先渲染页面,这确实是一个可行的优化方向,我们尝试调整代码位置:


this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>

调整位置后,页面内容“this is a test”迅速渲染显示,表面看问题似乎解决。然而:


this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>

在原有代码基础上增加一段逻辑后,发现控制台中的“hello world”依然迟迟无法输出。这表明前面的 JS 请求依然阻塞了后续代码的执行。这证实了一个关键结论:仅改变 JS 加载位置只能改善页面渲染时机,无法解决 JS 执行本身的阻塞问题。

实现js异步加载

我们的目标很明确:在页面加载过程中,允许控制台字符串输出,即实现“在请求外部 JS 的同时,继续执行后续 JS 代码”的异步加载效果。

业界最常见的做法是通过动态生成 script 标签来实现:


<body>
 this is a test
 <script type="text/javascript">
  ~function() {
   var s = document.createElement('script');
   s.src = 'http://china-addthis.googlecode.com/svn/trunk/addthis.js';
   document.body.appendChild(s);
  }();
 </script>
 <script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
 <script type="text/javascript">
  console.log('hello world');
 </script>
</body>

然而,这种方式存在一个缺陷:在动态脚本加载并执行完毕前,会阻止 onload 事件的触发。由于许多页面需要在 onload 事件中执行额外的渲染或初始化工作,这种阻塞仍会影响部分页面的初始化处理:


<body>
 this is a test
 <script type="text/javascript">
  ~function() {
   // function async_load() {
    var s = document.createElement('script');
    s.src = 'http://china-addthis.googlecode.com/svn/trunk/addthis.js';
    document.body.appendChild(s);
   // }
   // window.addEventListener('load', async_load, false);
  }();

  window.onload = function() {
   var txt = document.createTextNode(' hello world');
   document.body.appendChild(txt);
  };
 </script>
 <script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
</body>

如上述代码所示,若未正确处理,页面可能无法良好渲染“hello world”。解决方法是取消注释,将外部 JS 的加载逻辑绑定到 window.onload 事件中,使其在页面完全加载后才异步加载。这样便解决了阻塞 onload 事件触发的问题。

补充说明 DOMContentLoaded 与 OnLoad 事件的区别: - DOMContentLoaded:表示文档(document)解析完成,DOM 元素已可用,但图片、子框架等资源可能尚未加载完毕。 - OnLoad:表示页面所有资源(包括图片)均加载完毕,此时浏览器载入进度才停止。 这两个时间点将页面加载的生命周期划分为三个阶段。

上述动态脚本方案虽能解决问题,但 HTML5 提供了更简便的解决方案:async 属性!


this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' async='async'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>

async 是 HTML5 新增的属性,规定脚本一旦下载完毕即异步执行(无需等待解析完成)。

需注意,async 属性仅适用于外部脚本(即必须使用 src 属性指定源文件)。

defer 属性常与 async 一同被提及:


this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' defer='defer'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>

两者看似效果相似,但实际机制不同,我们需深入查看 defer 的定义。

早期 defer 仅支持 IE 的 hack 写法,HTML5 出现后开始全面支持。defer 属性规定脚本必须在页面解析完成后、DOMContentLoaded 事件触发前执行。该属性同样仅适用于外部脚本(使用 src 时)。注:IE 对 defer 的支持行为可能与标准有差异,此处不做深究,有兴趣者可查阅相关文档。

既然 async 和 defer 经常并列出现,有必要对二者进行辨析:

若不设置 async 或 defer(或赋值为 false),浏览器会立即执行当前 JS 脚本,阻塞后续脚本; 若设置 async 属性,文档后续元素的加载与当前 JS 的加载执行并行(异步); 若设置 defer 属性,文档后续元素的加载与 script.js 的加载并行(异步),但 script.js 的执行必须等待所有 DOM 元素解析完成,且在 DOMContentLoaded 事件触发之前完成。

参考一张广为流传的对比图:

同步加载的问题

图中蓝色线代表网络读取(下载)时间,红色线代表脚本执行时间,绿色线代表 HTML 解析过程。

此图揭示了以下核心要点(摘自 defer 和 async 的区别分析):

  1. defer 和 async 在网络读取(下载)阶段表现一致,均为异步(相对于 HTML 解析而言)。
  2. 两者的核心差异在于脚本下载完成后的执行时机。显然,defer 更符合常规应用脚本对加载和执行顺序的要求。
  3. 关于 defer,此图未完全展示的一点是:它严格按照脚本声明的加载顺序执行,这一特性值得开发者善加利用。
  4. async 则表现为“乱序执行”。由于加载与执行紧密相连,无论声明顺序如何,只要脚本加载完毕便会立即执行。
  5. 仔细分析,async 对于有依赖关系的应用脚本用处有限,因为它完全忽略依赖顺序(甚至是最基础的顺序执行)。但对于不依赖其他脚本、也不被其他脚本依赖的场景(如 Google Analytics 统计代码),async 非常合适。

个人理解(如有出入欢迎指正):defer 在异步加载中的应用广度并不比 async 广。async 意为“异步”,作用于脚本时,使脚本在下载完成后立即执行,这与动态插入 script 标签效果类似(区别在于 async 仅支持 HTML5,而动态插入法兼容性更好);defer 意为“延迟”,其作用与字面意思一致,即延迟脚本执行,待 DOM 元素加载完毕后按序执行脚本。正因为 defer 保证了顺序性,所以会带来另一个问题:


this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' defer='defer'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js' defer='defer'></script>
<script type="text/javascript" src='index.js' defer='defer'></script>
console.log('hello world');

若执行上述代码,控制台的“hello world”输出依然会延迟。因此,个人认为 async 更为好用;若需处理脚本依赖关系,建议选择 requirejs、seajs 等模块加载器。

总结

JavaScript 的异步加载还有其他实现方式,例如:AJAX eval(通过 AJAX 获取脚本内容,再利用 eval(xmlhttp.responseText) 执行)、iframe 嵌入方式等。

以上理解如有偏差,欢迎指正~感谢大家对本站的支持。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章