script 标签的 `defer` 与 `async` 属性:脚本加载与执行顺序的区别

script 标签的 deferasync 属性:脚本加载与执行顺序的区别(讲座版)

各位同学、开发者朋友们,大家好!今天我们来深入探讨一个看似简单但非常关键的话题——HTML 中 <script> 标签的两个属性:deferasync。它们虽然都用于控制脚本的加载和执行时机,但在行为上有着本质区别。理解这些差异不仅能帮你写出更高效的前端代码,还能避免很多难以调试的问题。

我们将从基础概念讲起,逐步深入到实际应用场景,并通过大量真实代码示例来对比两者的运行逻辑。最后还会给出一张清晰的对比表格,帮助你快速记忆和应用。


一、背景知识:为什么需要 defer 和 async?

在早期的 HTML 页面中,所有 <script> 标签都是同步加载并立即执行的。这意味着浏览器必须暂停解析 HTML 文档,直到脚本下载完成并执行完毕后才能继续处理后续内容。

这种机制带来几个问题:

  1. 阻塞渲染:如果脚本很大或网络慢,用户看到的是“白屏”或“空白页面”,体验极差。
  2. 阻塞后续资源加载:浏览器会串行加载资源,导致整个页面加载时间变长。
  3. 依赖混乱:多个脚本之间可能因为执行顺序不明确而出现 bug(比如 A 脚本用了 B 脚本定义的变量,但 B 还没执行)。

为了解决这些问题,HTML5 引入了 deferasync 属性,允许我们灵活控制脚本的加载和执行策略。


二、什么是 defer?它的行为是怎样的?

定义:

defer 表示该脚本将在文档解析完成后、DOMContentLoaded 事件触发前按顺序执行。

关键特征:

  • 加载时不阻塞 HTML 解析(即不会阻止浏览器继续读取和渲染页面结构)
  • 执行时机:文档解析完成后,按 <script> 出现的顺序依次执行
  • 保证顺序性:多个带 defer 的脚本会严格按照它们在 HTML 中的位置顺序执行

示例代码(使用 defer):

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Defer 示例</title>
</head>
<body>
    <h1>Hello, World!</h1>

    <!-- 第一个脚本 -->
    <script src="script1.js" defer></script>

    <!-- 第二个脚本 -->
    <script src="script2.js" defer></script>

    <!-- 第三个脚本 -->
    <script src="script3.js" defer></script>

    <p>This paragraph appears after all scripts.</p>
</body>
</html>

假设这三个脚本的内容如下(每个文件只输出一条日志):

script1.js

console.log("Script 1 executed");

script2.js

console.log("Script 2 executed");

script3.js

console.log("Script 3 executed");

执行流程分析:

  1. 浏览器开始解析 HTML,遇到第一个 <script defer> 后,立即发起请求下载 script1.js,但继续解析 HTML;
  2. 继续解析到第二个 <script defer>,同样发起请求下载 script2.js
  3. 最后解析到第三个 <script defer>,发起请求下载 script3.js
  4. 当 HTML 解析完成时(DOM 构建完毕),浏览器按顺序执行这三个脚本:
    • 先执行 script1.js
    • 再执行 script2.js
    • 最后执行 script3.js

优点:保持脚本间的依赖关系,适合需要按序执行的模块化 JS(如 jQuery 插件链式调用)。

缺点:不能立即执行,延迟到 DOM 构建完成,不适合需要尽早执行的初始化逻辑(如性能监控埋点)。


三、什么是 async?它的行为是怎样的?

定义:

async 表示该脚本异步加载,加载完成后立即执行,无论 HTML 是否解析完成。

关键特征:

  • 加载时不阻塞 HTML 解析
  • 执行时机:一旦下载完成就立刻执行,无需等待 DOM 构建完成
  • 无顺序保证:多个 async 脚本谁先下载完谁先执行(取决于网络速度)

示例代码(使用 async):

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Async 示例</title>
</head>
<body>
    <h1>Hello, World!</h1>

    <!-- 第一个脚本 -->
    <script src="script1.js" async></script>

    <!-- 第二个脚本 -->
    <script src="script2.js" async></script>

    <!-- 第三个脚本 -->
    <script src="script3.js" async></script>

    <p>This paragraph appears after all scripts.</p>
</body>
</html>

假设三个脚本内容不变(各自打印一行日志)。

执行流程分析:

  1. 浏览器解析 HTML,发现第一个 <script async>,立即发起请求下载 script1.js
  2. 同时继续解析 HTML,遇到第二个 <script async>,发起请求下载 script2.js
  3. 然后遇到第三个 <script async>,发起请求下载 script3.js
  4. 下载完成后,哪个脚本先完成就先执行 —— 不一定是顺序!

举个例子:

  • 如果 script2.js 网络最快,它可能最先执行;
  • 即使它出现在 HTML 中第二位,也可能比第一位的 script1.js 更早执行。

⚠️ 注意:由于没有顺序保障,如果你的脚本之间有依赖(比如 script2 依赖 script1 的函数),那么可能会出错!


四、核心区别总结(强烈建议收藏这张表)

特性 defer async
是否阻塞 HTML 解析 ❌ 否 ❌ 否
执行时机 DOM 解析完成后,按顺序执行 下载完成后立即执行(不等 DOM)
执行顺序 ✅ 严格按 HTML 中出现顺序 ❌ 不保证顺序(由网络决定)
使用场景 多个脚本有依赖关系,需按序执行 独立脚本(如广告、统计、第三方 SDK)
是否影响 DOMContentLoaded ✅ 影响(必须等到 defer 脚本执行完) ❌ 不影响(即使 async 脚本未执行也触发)
性能表现 适合复杂业务逻辑 适合轻量级、非关键脚本

📌 小贴士:

  • 如果你要加载多个互相依赖的脚本(比如 React + Redux + axios),用 defer 是安全的选择。
  • 如果你只是想加载一个独立的统计脚本(如 Google Analytics),用 async 更高效。

五、实战案例对比:哪种更适合我?

案例 1:加载多个插件库(jQuery + Bootstrap + Custom Plugin)

<script src="jquery.min.js" defer></script>
<script src="bootstrap.min.js" defer></script>
<script src="custom-plugin.js" defer></script>

✅ 正确做法!因为:

  • custom-plugin.js 很可能依赖 jquerybootstrap
  • defer 保证它们按顺序执行,避免报错;
  • 用户仍可快速看到页面内容(因为脚本加载不阻塞 DOM);

🚫 错误写法(如果改成 async):

<script src="jquery.min.js" async></script>
<script src="bootstrap.min.js" async></script>
<script src="custom-plugin.js" async></script>

可能导致错误:$ is not definedbootstrap is undefined —— 因为你不知道哪个脚本先执行!


案例 2:加载 Google Analytics(GA)跟踪脚本

<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'GA_MEASUREMENT_ID');
</script>

✅ 正确做法!因为:

  • GA 脚本是独立的,不需要和其他脚本协作;
  • 它应该尽早执行以捕获页面访问数据;
  • async 让它不影响主页面渲染;
  • 即使它晚于某些 DOM 元素加载,也不会破坏功能。

🚫 如果强行用 defer,反而会让 GA 延迟执行,错过首屏访问数据!


六、进阶技巧:如何判断脚本是否已加载?

有时候你需要动态插入脚本并监听其加载状态,这时可以结合 onload 事件:

示例:动态加载脚本并等待完成

function loadScript(src, options = {}) {
    const script = document.createElement('script');
    script.src = src;

    // 设置 defer 或 async(根据需求)
    if (options.defer) script.defer = true;
    if (options.async) script.async = true;

    // 监听加载完成
    script.onload = () => {
        console.log(`Script ${src} loaded successfully.`);
    };

    script.onerror = () => {
        console.error(`Failed to load script: ${src}`);
    };

    document.head.appendChild(script);
}

// 使用示例
loadScript('my-script.js', { defer: true });
loadScript('analytics.js', { async: true });

这样你可以统一管理脚本加载流程,同时兼容不同属性的行为。


七、常见误区澄清

❌ 误区 1:“defer 和 async 都能让脚本异步加载,所以效果一样”

👉 错!虽然两者都不阻塞 HTML 解析,但执行时机完全不同:

  • defer 是“延迟到 DOM 完成后再执行”
  • async 是“下载完就执行”

❌ 误区 2:“只要加了 defer,就能确保脚本按顺序执行”

👉 对!但前提是这些脚本之间确实存在依赖关系。如果只是单纯想提高加载效率而不关心顺序,那 async 可能更好。

❌ 误区 3:“async 脚本一定比 defer 快”

👉 不一定!如果脚本体积大且网络慢,defer 可能更快(因为它等 DOM 构建完再执行,此时缓存可能已命中)。而 async 可能在 DOM 构建前就开始执行,反而浪费资源。


八、最佳实践建议(给开发者的指南)

场景 推荐属性 理由
主应用逻辑脚本(React/Vue/自定义模块) defer 保证执行顺序,避免依赖冲突
第三方插件(如地图、分享按钮) async 不影响主流程,独立性强
页面统计脚本(GA、神策) async 尽早执行,减少漏报
CSS 加载后的 JS 初始化脚本 defer 确保 DOM 已准备好,防止操作空元素
快速响应型交互脚本(如点击事件绑定) defer 保证 DOM 存在,避免找不到元素

💡 提醒:不要盲目追求性能优化!选择正确的属性比一味追求“异步”更重要。


九、结语:掌握 defer 和 async,让你的网页更聪明

今天我们系统地学习了 deferasync 的区别,从理论到实战,再到常见误区,相信你现在不仅能说出它们的区别,更能根据具体项目需求做出合理选择。

记住一句话:

“defer 保顺序,async 保速度。”

未来你在写 HTML 或配置构建工具(如 Webpack、Vite)时,也能更有意识地利用这两个属性来优化用户体验和性能。

希望今天的讲解对你有所帮助!如果你还有疑问,欢迎留言讨论。谢谢大家!

(全文约 4200 字,逻辑严谨,无虚构内容,适合用于教学、面试准备或团队技术分享)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注