Skip to content

script 标签与 JavaScript

<script> 元素用于在 HTML 中嵌入或引入 JavaScript 代码。它的加载和执行方式对页面性能有重大影响,理解 asyncdefertype="module" 等属性是前端开发的核心知识。

前置知识

阅读本节前,建议先了解:preload / prefetch / preconnect

基础概念

script 的两种使用方式

1. 内联脚本:直接在标签内写 JavaScript 代码:

html
<script>
  console.log('Hello, World!');
</script>

2. 外部脚本:通过 src 属性引入外部 JS 文件:

html
<script src="app.js"></script>

script 的加载行为

JavaScript 的加载和执行会阻塞 HTML 的解析。这是默认行为,也是新手最容易遇到的问题。

解析 HTML → 遇到 script → 下载 JS → 执行 JS → 继续解析 HTML

语法

基本属性

属性说明
src外部脚本 URLURL
type脚本类型text/javascript(默认)、moduleimportmap
async异步加载并执行布尔属性
defer延迟到 DOM 解析完成后执行布尔属性
nomodule不支持 module 时执行布尔属性
crossorigin跨域请求方式anonymoususe-credentials
integrity子资源完整性校验哈希值
charset字符编码编码名称(极少使用)

详细说明

加载执行顺序详解

默认行为(同步加载)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>同步脚本</title>
  <script src="large-script.js"></script>  <!-- 阻塞! -->
  <script src="another-script.js"></script>  <!-- 阻塞! -->
</head>
<body>
  <!-- 这些内容要等所有脚本下载并执行完才会显示 -->
  <h1>页面内容</h1>
</body>
</html>

执行时序图:

HTML解析 ──┬─→ 下载 large-script.js ──→ 执行 ──┬─→ 下载 another-script.js ──→ 执行 ──┬─→ 继续解析HTML
          │                                    │                                        │
          └────────────────────────────────────┴────────────────────────────────────┘
                                页面空白,用户只能等待

async 异步加载

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>async 脚本</title>
  <script src="analytics.js" async></script>
  <script src="ad.js" async></script>
</head>
<body>
  <h1>页面内容</h1>
  <!-- 页面内容可以正常显示,脚本在后台下载 -->
</body>
</html>

执行时序图:

HTML解析 ─────────────────────────────────────────────────────→ 继续解析
  │                                                                │
  ├─→ 下载 analytics.js ──→ (下载完成,立即执行)                    │
  ├─→ 下载 ad.js ─────────→ (下载完成,立即执行)                    │
  └────────────────────────────────────────────────────────────────┘
        不阻塞 HTML 解析,但执行时仍会阻塞
        执行顺序取决于谁先下载完(不确定)

特点:

  • 不阻塞 HTML 解析
  • 下载完成后立即执行(可能在 HTML 解析完成之前)
  • 多个 async 脚本的执行顺序不确定
  • 适用于独立的脚本(如统计代码、广告代码)

defer 延迟执行

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>defer 脚本</title>
  <script src="app.js" defer></script>
  <script src="utils.js" defer></script>
</head>
<body>
  <h1>页面内容</h1>
  <p>页面内容先显示,脚本在 DOM 解析完成后按顺序执行。</p>
</body>
</html>

执行时序图:

HTML解析 ─────────────────────────────────────→ DOM解析完成 ──→ 触发DOMContentLoaded
  │                                                  │               │
  ├─→ 下载 app.js(后台,不阻塞)                      │          执行 app.js
  ├─→ 下载 utils.js(后台,不阻塞)                    │          执行 utils.js
  └──────────────────────────────────────────────────┘
        不阻塞 HTML 解析,在 DOM 解析完成后按顺序执行

特点:

  • 不阻塞 HTML 解析
  • 在 DOM 解析完成后、DOMContentLoaded 之前执行
  • 多个 defer 脚本按声明顺序执行
  • 适用于需要操作 DOM 的脚本

三种加载方式对比

特性无属性(同步)asyncdefer
阻塞 HTML 解析
执行时机下载完立即下载完立即DOM 解析完成后
执行顺序按声明顺序不确定按声明顺序
访问 DOM部分可用不确定完全可用
适用场景必须的初始化脚本独立脚本(统计、广告)应用主脚本
推荐程度谨慎使用特定场景强烈推荐

type="module"(ES 模块)

html
<script type="module" src="app.mjs"></script>

ES 模块脚本的行为类似于 defer

  • 自动 defer(不阻塞 HTML 解析)
  • 在 DOM 解析完成后执行
  • 按声明顺序执行
  • 自动使用严格模式
  • 支持顶层 await
  • 支持 import / export
html
<!-- ES 模块(自动 defer) -->
<script type="module" src="app.mjs"></script>

<!-- 普通脚本配合 nomodule(回退方案) -->
<script nomodule src="app.bundle.js"></script>

module 与 defer 的区别

<script type="module"> 默认具有 defer 行为。但如果同时设置了 async,则变为 async 行为。<script type="module" async src="app.mjs">

nomodule 属性

nomodule 用于为不支持 ES 模块的浏览器提供回退方案:

html
<!-- 现代浏览器:执行模块版本 -->
<script type="module" src="app.mjs"></script>

<!-- 旧浏览器(不支持 module):执行打包版本 -->
<script nomodule src="app.bundle.js"></script>

现代浏览器会忽略 nomodule 脚本,旧浏览器会忽略 type="module" 脚本。

crossorigin 属性

当使用跨域脚本(CDN 上的 JS 文件)时:

html
<!-- 不带 crossorigin:不发送凭据(Cookie 等),无法获取详细的错误信息 -->
<script src="https://cdn.example.com/lib.js"></script>

<!-- crossorigin="anonymous":不发送凭据,但可以获取详细错误信息 -->
<script src="https://cdn.example.com/lib.js" crossorigin="anonymous"></script>

<!-- crossorigin="use-credentials":发送凭据 -->
<script src="https://cdn.example.com/lib.js" crossorigin="use-credentials"></script>

integrity 属性(SRI)

子资源完整性(Subresource Integrity,SRI)用于验证外部脚本是否被篡改:

html
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-abc123def456..."
  crossorigin="anonymous"
></script>

integrity 的值是文件内容的哈希值。浏览器在下载文件后会计算哈希并与声明值比对,不匹配则拒绝执行。

生成 integrity 值

可以使用命令行工具生成:openssl dgst -sha384 -binary lib.js | openssl base64 -A 或使用在线工具 https://www.srihash.org/

实战示例

推荐的脚本加载方式

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>脚本加载最佳实践</title>

  <!-- 预连接 CDN 域名 -->
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>

  <!-- 关键的同步脚本(如果确实需要) -->
  <script src="/js/critical-config.js"></script>

  <!-- 外部样式 -->
  <link rel="stylesheet" href="/css/style.css">

  <!-- 应用主脚本:使用 defer(推荐) -->
  <script src="/js/app.js" defer></script>
  <script src="/js/utils.js" defer></script>

  <!-- 第三方分析脚本:使用 async -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>

  <!-- ES 模块脚本 -->
  <script type="module" src="/js/module-app.mjs"></script>

  <!-- 回退脚本 -->
  <script nomodule src="/js/bundle.js" defer></script>
</head>
<body>
  <h1>脚本加载最佳实践</h1>
  <p>页面内容可以快速显示。</p>
</body>
</html>

脚本执行顺序演示

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>脚本执行顺序</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
    .log { background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 14px; margin: 10px 0; white-space: pre-line; }
    button { padding: 8px 16px; margin: 5px; cursor: pointer; background: #0066cc; color: white; border: none; border-radius: 4px; }
  </style>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      addLog('DOMContentLoaded 事件触发');
    });
    window.addEventListener('load', function() {
      addLog('load 事件触发');
    });
    function addLog(msg) {
      var el = document.getElementById('log');
      if (el) {
        var time = new Date().toISOString().split('T')[1];
        el.textContent += '[' + time + '] ' + msg + '\n';
      }
    }
  </script>
</head>
<body>
  <h1>脚本执行顺序演示</h1>
  <div id="log" class="log"></div>
  <button onclick="addLog('按钮被点击')">测试点击</button>

  <!-- defer 脚本:DOM 解析完成后按顺序执行 -->
  <script>
    addLog('内联脚本 1(同步,在 head 中)');
  </script>
  <script src="data:text/javascript,document.addEventListener('DOMContentLoaded',function(){parent.addLog('defer 脚本 1 执行')})" defer></script>
  <script src="data:text/javascript,document.addEventListener('DOMContentLoaded',function(){parent.addLog('defer 脚本 2 执行')})" defer></script>

  <p>DOM 内容已解析。</p>

  <script>
    addLog('内联脚本 2(同步,在 body 末尾)');
  </script>
</body>
</html>

动态加载脚本

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动态加载脚本</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
    button { padding: 8px 16px; margin: 5px; cursor: pointer; background: #0066cc; color: white; border: none; border-radius: 4px; }
    .log { background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 14px; margin: 10px 0; white-space: pre-line; }
  </style>
</head>
<body>
  <h1>动态加载脚本</h1>
  <div>
    <button id="btn-defer">动态加载 defer 脚本</button>
    <button id="btn-async">动态加载 async 脚本</button>
  </div>
  <div id="log" class="log"></div>

  <script>
    var log = document.getElementById('log');

    function addLog(msg) {
      log.textContent += msg + '\n';
    }

    // 动态加载脚本(默认行为)
    function loadScript(url, isAsync, isDefer) {
      addLog('开始加载: ' + url);
      var script = document.createElement('script');
      script.src = url;
      if (isAsync) script.async = true;
      if (isDefer) script.defer = true;
      script.onload = function() {
        addLog('加载完成: ' + url);
      };
      document.head.appendChild(script);
    }

    document.getElementById('btn-defer').addEventListener('click', function() {
      loadScript('data:text/javascript,parent.addLog("defer 脚本执行")', false, true);
    });

    document.getElementById('btn-async').addEventListener('click', function() {
      loadScript('data:text/javascript,parent.addLog("async 脚本执行")', true, false);
    });
  </script>
</body>
</html>

注意事项

1. head 中的同步脚本会阻塞渲染

html
<!-- 不推荐:head 中的同步脚本 -->
<head>
  <script src="large-script.js"></script>  <!-- 阻塞! -->
</head>

<!-- 推荐 -->
<head>
  <script src="app.js" defer></script>  <!-- 不阻塞 -->
</head>

2. defer 脚本不能保证在 load 之前执行

如果 defer 脚本引用了外部资源(如 import),它可能在 load 事件之后才执行。

3. 内联脚本不支持 async/defer

asyncdefer 只对有 src 属性的外部脚本有效,对内联脚本无效。

4. document.write 在 async/defer 脚本中不可靠

javascript
// 在 async 或 defer 脚本中使用 document.write 可能导致问题
document.write('<p>这段文字可能不会被正确插入</p>');

最佳实践

1. 默认使用 defer

html
<script src="app.js" defer></script>

2. 独立脚本使用 async

html
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>

3. 使用 ES 模块

html
<script type="module" src="app.mjs"></script>

4. 脚本放在 body 末尾(旧项目兼容方案)

html
<body>
  <!-- 页面内容 -->
  <script src="app.js"></script>
</body>

5. 使用 SRI 保护跨域脚本

html
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-xxx"
  crossorigin="anonymous"
></script>

下一节

继续学习:style 内联样式

参考链接