本文深入解析HTML中script标签的defer与async特性。defer确保脚本在文档解析完成后、DOMContentLoaded之前按序执行,不阻塞渲染但可能阻塞事件;async则异步加载并立即执行,顺序不确定。文章详细对比了两者在IE及现代浏览器中的支持情况、潜在Bug(如IE9的DOM修改导致执行顺序错乱)及适用场景,帮助开发者优化页面加载性能。
引言
defer和async是JavaScript开发者常接触却易混淆的两个属性。从字面看,它们分别代表“延迟脚本”和“异步脚本”。但细节往往被忽视,例如defer脚本究竟何时执行?内部脚本是否支持defer?除了延迟执行外还有什么特殊行为?本文结合MDN文档及相关研究,全面解析defer与async,助开发者精准掌握。
1 引言
在前文《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中,我们指出JavaScript执行会阻塞页面解析、渲染及其他资源下载。由于JS单线程特性,代码通常按从上到下顺序执行。但在document.write或动态脚本等场景下,执行顺序可能改变。defer和async正是处理这些“非正常”情况的关键机制。
开发中,我们最关注的阻塞影响用户体验的方面包括:
[1]页面解析和渲染的阻塞
[2]页面初始化脚本(通常绑定DOMContentLoaded事件,包含与用户交互最相关的代码,期望最先执行)
[3]页面外部资源下载(如图片)的阻塞
若耗时脚本阻塞了上述三者,将严重损害网页性能与用户体验。
defer与async特性旨在解决或缓解阻塞对页面体验的影响,下文将从多维度深入剖析这两项特性。
[1]延迟或异步脚本的执行时机?对页面阻塞情况如何?
[2]内部脚本和外部脚本是否均支持延迟或异步?
[3]浏览器支持情况如何?是否存在相关Bug?
[4]使用这两个特性时需注意哪些事项?
defer特性详解
2 defer特性
2.1 关于defer脚本的执行时机
defer是HTML4规范定义的扩展属性,最初仅IE4+和Firefox3.5+支持,写法为defer="defer"。其含义为“延迟”,即脚本下载后不立即执行,而是等待页面解析完毕后再执行。HTML4标准对defer的描述如下:
defer:When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.
这意味着设置defer后,浏览器会认为该脚本不生成文档内容(如不使用document.write),从而继续解析和渲染页面。MDN对defer的关键描述为:
defer:If the async attribute is not present but the defer attribute is present, then the script is executed when the page has finished parsing.
根据标准,defer脚本不阻塞页面解析,而是在解析结束后执行。但耗时操作仍可能阻塞外部资源下载。关键在于,defer脚本会在DOMContentLoaded事件之前执行,因此它会阻塞DOMContentLoaded绑定的脚本。下图有助于理解defer的执行时机:

根据标准,内部脚本不支持defer,但IE9及以下浏览器提供了内部脚本的defer支持。
2.2 defer的浏览器支持情况
以下是defer特性的浏览器支持概况:

需注意,IE9及以下浏览器存在一个Bug,将在后续DEMO中详细说明。
2.3 DEMO:defer特性的功能验证
参考Olivier Rochard在《the script defer attribute》中的验证方式,我们进行如下测试:
首先准备6个外部脚本:
1.js:
test += "我是head外部脚本\n";
2.js
test += "我是body外部脚本\n";
3.js
test += "我是底部外部脚本\n";
defer1.js
test += "我是head外部延迟脚本\n";
defer2.js
test += "我是body外部延迟脚本\n";
defer3.js
test += "我是底部外部延迟脚本\n";
HTML代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>defer attribute test</title>
<script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script>
<script type="text/javascript">var test = "";</script>
<script src="defer1.js" type="text/javascript" defer="defer"></script>
<script src="1.js" type="text/javascript"></script>
<script defer="defer">
test += "我是head延迟内部脚本\n";
</script>
<script>
test += "我是head内部脚本\n";
</script>
</head>
<body>
<button id="test">点击一下</button>
<script src="defer2.js" type="text/javascript" defer="defer"></script>
<script src="2.js" type="text/javascript"></script>
</body>
<script src="defer3.js" type="text/javascript" defer="defer"></script>
<script src="3.js" type="text/javascript"></script>
<script>
$(function(){
test += "我是DOMContentLoaded里面的脚本\n";
})
window.onload = function(){
test += "我是window.onload里面的脚本\n";
var button = document.getElementById("test");
button.onclick = function(){
alert(test);
}
}
</script>
</html>
为便于实现DOMContentLoaded事件,代码引入了jQuery(后续文章将介绍如何自行实现兼容的DOMContentLoaded)。我们在head、body内部及底部分别引入延迟脚本和正常脚本,并通过全局字符串记录执行状态。各浏览器执行结果如下:
| IE7 | IE9 | IE10 | CHROME | firefox |
|
我是head外部脚本 |
我是head外部脚本 |
我是head外部脚本 |
我是head外部脚本 |
|
结果显示,仅IE9及以下浏览器支持内部延迟脚本。此外,所有defer脚本均在DOMContentLoaded事件触发前执行,因此会阻塞DOMContentLoaded中的脚本。
2.4 DEMO:IE<=9的defer特性Bug
从2.3节DEMO可见,defer脚本通常按添加顺序依次执行。但在IE<=9中存在Bug:若文档中引入多个defer脚本,且前一个脚本调用了appendChild、innerHTML、insertBefore或replaceChild等修改DOM的接口,后续脚本可能先于该脚本执行。相关GitHub Issue见:https://github.com/h5bp/lazyweb-requests/issues/42
我们通过DEMO验证,首先修改1.js代码(此代码仅为模拟,实际存在极大性能问题):
document.body.innerHTML = "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; document.body.innerHTML += "<div id='div'>我是后来加入的</div>"; alert("我是第1个脚本");
2.js
alert("我是第2个脚本");
修改HTML代码为:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>defer bug in IE=9 test</title>
<script src="1.js" type="text/javascript" defer="defer"></script>
<script src="2.js" type="text/javascript" defer="defer"></script>
</head>
<body>
</body>
</html>
正常浏览器中,弹窗顺序应为:我是第1个脚本 -> 我是第2个脚本。然而在IE<=9中,执行结果却是:我是第2个脚本 -> 我是第1个脚本,这验证了该Bug的存在。
2.5 defer总结
总结前需注意一点:正如标准所述,defer脚本中不应出现document.write操作,浏览器会直接忽略此类操作。
总体而言,defer的作用与将脚本置于页面底部相似。但由于IE<=9的Bug,若页面包含多个defer脚本,执行顺序可能被打乱,导致代码依赖出错。因此实际项目中较少使用defer,通常将脚本置于页面底部即可替代其功能。
async特性详解
3 async特性
3.1 关于async脚本的执行时机
async是HTML5引入的特性,写法为:async="async"。标准对async的描述如下:
async:If the async attribute is present, then the script will be executed asynchronously, as soon as it is available.
需指出,这里的“异步”指异步加载而非异步执行。即浏览器遇到async脚本标签时,会异步加载(主要是下载过程),一旦加载完毕即执行代码。执行过程仍是同步的,即阻塞的。下图综合展示了defer与async的区别:

由此可见,async脚本的执行时机不确定,因为加载完成时间不可预知。通过以下DEMO可直观感受:
async1.js
alert("我是异步的脚本");
HTML代码:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>async attribute test</title>
<script src="/delayfile.php?url=http://localhost/js/load/async1.js&delay=2" async="async" type="text/javascript"></script>
<script>
alert("我是同步的脚本");
</script>
</head>
<body>
</body>
</html>
此处借用了《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中的delayfile脚本提供延迟。在支持async的浏览器中,弹窗顺序通常为:我是同步的脚本 -> 我是异步的脚本。
3.2 async的浏览器支持情况
以下是async特性的浏览器支持概况:

可见,仅IE10+支持async,Opera Mini不支持。此外,async不支持内部脚本。
3.3 async总结
async指异步脚本,即脚本异步加载,加载过程不阻塞。但async脚本的执行时机和顺序均不确定。因此,使用async的脚本应不依赖任何其他代码(如第三方统计或广告代码),否则可能导致执行错误。
defer与async的优先级
4 defer和async的优先级问题
这一点较为直观,标准规定:
[1]若<script>元素同时定义defer和async,则按async处理(注意:不支持async的浏览器会忽略async特性)
[2]若仅定义defer,则按延迟脚本处理
[3]若未定义defer和async,则按正常情况处理,即脚本立即加载和执行