我们来系统且深入地剖析 <script> 标签,从 defer 属性开始,并延展到所有相关的属性。
这不仅仅是关于“功能是什么”,更是关于“为什么需要它”以及“在何种场景下做出最优选择”。
defer 的核心作用:延迟且有序的执行
defer 的全称是 "defer execution"。它的核心目的,是在不阻塞页面渲染的前提下,加载并执行 JavaScript 代码。
要理解 defer,首先要明白浏览器默认是如何处理 <script> 标签的。
1. 默认行为(无任何属性)
当浏览器解析 HTML 文档时,从上到下逐行处理。如果遇到一个普通的 <script src="..."></script> 标签:
-
停止解析:浏览器会立即暂停解析后续的 HTML。
-
下载脚本:它会发起请求去下载该脚本文件。
-
执行脚本:下载完成后,立即执行这个脚本。
-
继续解析:脚本执行完毕后,才继续解析剩余的 HTML。
这个过程是阻塞式 (Blocking) 的。如果脚本很大或者网络很慢,用户就会长时间看到一个白屏,因为浏览器被卡住,无法渲染页面的其余部分。这带来了极差的用户体验。
(这是一个简化的示意图,红色代表阻塞)
2. defer 的解决方案
为了解决上述问题,defer 属性应运而生。当你在 script 标签上加上 defer 时,行为会变成这样:
-
并行下载:浏览器看到
<script defer src="..."></script>,会立即开始异步下载脚本文件,但不会停止解析后续的 HTML。下载过程和 HTML 解析是并行的。 -
延迟执行:脚本下载完成后并不会立即执行,而是会等待整个 HTML 文档都被解析完毕(即
DOMContentLoaded事件触发之前)。 -
有序执行:如果页面上有多个带
defer的脚本,它们会严格按照在 HTML 中出现的顺序来执行。第一个defer脚本总是先于第二个defer脚本执行。
defer 的核心优势:
-
非阻塞渲染:下载过程不影响页面渲染,用户能更快地看到完整页面结构。
-
DOM可用:执行时,整个 DOM 树已经构建完毕,脚本可以安全地访问页面上的任何元素。
-
执行顺序保证:对于有依赖关系的脚本(例如,
jquery.js必须在main.js之前执行),defer能够确保正确的执行顺序。
defer 以外的其它 Script 标签属性
除了 defer,<script> 标签还有一系列非常重要的属性。其中,与 defer 最常比较的是 async。
async:异步无序的执行
async (asynchronous) 属性也用于实现非阻塞的脚本加载,但它和 defer 有一个关键区别。
-
并行下载:和
defer一样,async脚本的下载过程与 HTML 解析并行,不阻塞页面渲染。 -
立即执行:关键区别来了。一旦
async脚本下载完成,它会立即执行,而不管 HTML 文档是否解析完毕。这可能会暂停 HTML 的解析。 -
无序执行:如果有多个
async脚本,它们的执行顺序是不可预测的。哪个先下载完,哪个就先执行。这被称为“竞争”或“竞速”状态。
async 的适用场景:
它适用于那些完全独立、不依赖于其他脚本、也不需要操作 DOM 的脚本。最典型的例子:
-
网站分析脚本 (如 Google Analytics)
-
广告脚本
-
各种追踪器
这些脚本的共同点是:它们自己运行自己的,不关心页面结构,也不影响其他功能。早点执行或晚点执行都无所谓。
对比与抉择:defer vs async vs 默认
为了彻底清晰,我们用一个表格来总结:
| 属性 | HTML解析时下载 | 阻塞HTML解析? | 脚本执行时机 | 执行顺序 | DOM是否完备 | 最佳用途 |
|---|---|---|---|---|---|---|
| 无 | 否(暂停解析去下载) | 是(下载和执行都阻塞) | 下载后立即执行 | 严格按顺序 | 不一定,取决于脚本位置 | 基本不推荐使用 |
defer |
是(并行下载) | 否 | HTML解析完毕后,DOMContentLoaded前 |
严格按顺序 | 是 | 需要操作DOM或有依赖关系的脚本 |
async |
是(并行下载) | 否(但执行时可能暂停) | 下载后立即执行 | 无序,谁先下完谁先执行 | 不保证 | 独立、无依赖的第三方脚本(如分析、广告) |
我的选择建议
对于现代 Web 开发,你应该遵循以下原则:
-
defer是首选和默认选项:对于所有对页面功能至关重要的脚本(比如你的应用主逻辑app.js、UI框架vue.js等),都应该使用defer并将它们放在<head>中。这既能获得最佳的加载性能,又能保证脚本逻辑的稳定可靠。 -
async用于特定场景:只在你加载那些完全独立的第三方脚本时,才考虑使用async。 -
避免无属性的脚本:尽量避免在
<head>中使用无属性的<script>。如果你确实有极少数情况,需要在页面渲染的某个特定点立即执行一段代码(比如一个修复早期渲染问题的 polyfill),可以考虑将其放在<body>的末尾,但这通常不如defer优雅。
其他重要属性
除了 defer 和 async,还有一些属性你也应该了解。
-
src(source)-
作用:指定外部脚本文件的 URL。这是最基本、最常用的属性。如果一个
<script>标签没有src属性,那么浏览器会将其视为内联脚本,并执行标签内的代码。 -
示例:
<script src="/path/to/script.js"></script>
-
-
type-
作用:声明脚本的 MIME 类型。在 HTML5 中,其默认值是
text/javascript,所以你通常可以省略它。 -
重要特例
type="module":这是现代 JavaScript 的一个重要属性。它告诉浏览器这个脚本是一个 ES 模块。-
模块脚本默认行为类似
defer(异步加载,DOM 解析后执行),无需再额外添加defer。 -
它有自己的顶级作用域,不会污染全局命名空间。
-
可以使用
import和export关键字。
-
-
示例:
<script type="module" src="main.mjs"></script>
-
-
nomodule-
作用:这是一个布尔属性,用于为不支持 ES 模块的老旧浏览器提供一个回退脚本。支持
type="module"的现代浏览器会忽略带有nomodule属性的脚本。 -
示例:
HTML
<script type="module" src="app.mjs"></script> <script nomodule src="legacy-app.js"></script>
-
-
integrity(Subresource Integrity - SRI)-
作用:一个重要的安全特性。它允许你提供一个文件内容的哈希值。浏览器在下载脚本后,会计算其哈希值并与你提供的值进行比对。如果不匹配,浏览器将拒绝执行该脚本。
-
为什么需要?:这可以防止 CDN(内容分发网络)被攻击者篡改后,向你的用户提供恶意脚本。
-
示例:
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
-
-
crossorigin-
作用:当脚本从一个与你的网站不同的源(域名)加载时,该属性用于配置 CORS(跨源资源共享)请求。
-
anonymous:默认值。发送请求时不携带任何身份凭证(如 Cookie 或 HTTP 认证)。 -
use-credentials:发送请求时会携带身份凭证。 -
为什么需要?:如果你想捕获跨域脚本中发生的错误详情(而不是只得到一个模糊的 "Script error."),你就必须设置
crossorigin="anonymous"。
-
-
referrerpolicy- 作用:一个更精细的隐私控制属性。它用于控制在请求脚本时,HTTP
Referer头部会发送哪些信息。例如,no-referrer表示完全不发送来源信息。
- 作用:一个更精细的隐私控制属性。它用于控制在请求脚本时,HTTP
总结
defer 是一个用于优化页面加载性能和保证脚本执行稳定性的关键属性。它通过异步下载和延迟有序执行的机制,解决了传统 <script> 标签阻塞页面渲染的痛点。
在现代开发中,最佳实践是将你的主要脚本放在 <head> 中并一律使用 defer,而将独立的第三方脚本(如分析工具)使用 async。同时,结合使用 type="module"、integrity 等属性,可以构建一个既高效又安全的现代化 Web 应用。