script 标签与 JavaScript
<script> 元素用于在 HTML 中嵌入或引入 JavaScript 代码。它的加载和执行方式对页面性能有重大影响,理解 async、defer、type="module" 等属性是前端开发的核心知识。
前置知识
阅读本节前,建议先了解:preload / prefetch / preconnect
基础概念
script 的两种使用方式
1. 内联脚本:直接在标签内写 JavaScript 代码:
<script>
console.log('Hello, World!');
</script>2. 外部脚本:通过 src 属性引入外部 JS 文件:
<script src="app.js"></script>script 的加载行为
JavaScript 的加载和执行会阻塞 HTML 的解析。这是默认行为,也是新手最容易遇到的问题。
解析 HTML → 遇到 script → 下载 JS → 执行 JS → 继续解析 HTML语法
基本属性
| 属性 | 说明 | 值 |
|---|---|---|
src | 外部脚本 URL | URL |
type | 脚本类型 | text/javascript(默认)、module、importmap |
async | 异步加载并执行 | 布尔属性 |
defer | 延迟到 DOM 解析完成后执行 | 布尔属性 |
nomodule | 不支持 module 时执行 | 布尔属性 |
crossorigin | 跨域请求方式 | anonymous、use-credentials |
integrity | 子资源完整性校验 | 哈希值 |
charset | 字符编码 | 编码名称(极少使用) |
详细说明
加载执行顺序详解
默认行为(同步加载)
<!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 异步加载
<!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 延迟执行
<!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 的脚本
三种加载方式对比
| 特性 | 无属性(同步) | async | defer |
|---|---|---|---|
| 阻塞 HTML 解析 | 是 | 否 | 否 |
| 执行时机 | 下载完立即 | 下载完立即 | DOM 解析完成后 |
| 执行顺序 | 按声明顺序 | 不确定 | 按声明顺序 |
| 访问 DOM | 部分可用 | 不确定 | 完全可用 |
| 适用场景 | 必须的初始化脚本 | 独立脚本(统计、广告) | 应用主脚本 |
| 推荐程度 | 谨慎使用 | 特定场景 | 强烈推荐 |
type="module"(ES 模块)
<script type="module" src="app.mjs"></script>ES 模块脚本的行为类似于 defer:
- 自动 defer(不阻塞 HTML 解析)
- 在 DOM 解析完成后执行
- 按声明顺序执行
- 自动使用严格模式
- 支持顶层
await - 支持
import/export
<!-- 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 模块的浏览器提供回退方案:
<!-- 现代浏览器:执行模块版本 -->
<script type="module" src="app.mjs"></script>
<!-- 旧浏览器(不支持 module):执行打包版本 -->
<script nomodule src="app.bundle.js"></script>现代浏览器会忽略 nomodule 脚本,旧浏览器会忽略 type="module" 脚本。
crossorigin 属性
当使用跨域脚本(CDN 上的 JS 文件)时:
<!-- 不带 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)用于验证外部脚本是否被篡改:
<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/
实战示例
推荐的脚本加载方式
<!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>脚本执行顺序演示
<!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>动态加载脚本
<!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 中的同步脚本会阻塞渲染
<!-- 不推荐: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
async 和 defer 只对有 src 属性的外部脚本有效,对内联脚本无效。
4. document.write 在 async/defer 脚本中不可靠
// 在 async 或 defer 脚本中使用 document.write 可能导致问题
document.write('<p>这段文字可能不会被正确插入</p>');最佳实践
1. 默认使用 defer
<script src="app.js" defer></script>2. 独立脚本使用 async
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>3. 使用 ES 模块
<script type="module" src="app.mjs"></script>4. 脚本放在 body 末尾(旧项目兼容方案)
<body>
<!-- 页面内容 -->
<script src="app.js"></script>
</body>5. 使用 SRI 保护跨域脚本
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-xxx"
crossorigin="anonymous"
></script>下一节
继续学习:style 内联样式