Script 标签位置对页面加载的影响

script 放在 head 和 body 底部的区别及最佳实践

问题

Script 标签放在 <head> 中和 <body> 底部有什么区别?

解答

放在 <head>

<!DOCTYPE html>
<html>
<head>
  <title>Script in Head</title>
  <!-- 浏览器解析到这里会暂停 HTML 解析,下载并执行 JS -->
  <script src="app.js"></script>
</head>
<body>
  <!-- JS 执行完毕后才会渲染这部分内容 -->
  <div id="app">Hello World</div>
</body>
</html>

问题

  • 阻塞 HTML 解析,页面白屏时间长
  • JS 执行时 DOM 还未构建,无法操作 DOM 元素
// app.js - 放在 head 中执行
const app = document.getElementById('app');
console.log(app); // null,因为 DOM 还没解析到这个元素

放在 <body> 底部

<!DOCTYPE html>
<html>
<head>
  <title>Script at Bottom</title>
</head>
<body>
  <div id="app">Hello World</div>
  <!-- DOM 已经解析完成,可以安全操作 -->
  <script src="app.js"></script>
</body>
</html>
// app.js - 放在 body 底部执行
const app = document.getElementById('app');
console.log(app); // <div id="app">Hello World</div>

使用 defer 和 async

现代方案是在 <head> 中使用 deferasync 属性:

<!DOCTYPE html>
<html>
<head>
  <title>Modern Approach</title>
  <!-- defer: 并行下载,DOM 解析完成后按顺序执行 -->
  <script defer src="app.js"></script>
  
  <!-- async: 并行下载,下载完立即执行(不保证顺序) -->
  <script async src="analytics.js"></script>
</head>
<body>
  <div id="app">Hello World</div>
</body>
</html>

执行时机对比

普通 script(head):
HTML 解析 → 暂停 → 下载 JS → 执行 JS → 继续解析 HTML → DOMContentLoaded

普通 script(body 底部):
HTML 解析完成 → 下载 JS → 执行 JS → DOMContentLoaded

defer:
HTML 解析(同时下载 JS)→ 解析完成 → 执行 JS → DOMContentLoaded

async:
HTML 解析(同时下载 JS)→ 下载完立即执行(可能打断解析)→ DOMContentLoaded

完整示例

<!DOCTYPE html>
<html>
<head>
  <title>Script Loading Demo</title>
  <!-- 第三方统计,不依赖 DOM,用 async -->
  <script async src="https://example.com/analytics.js"></script>
  
  <!-- 主应用代码,需要 DOM,用 defer -->
  <script defer src="vendor.js"></script>
  <script defer src="app.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

关键点

  • head 中普通 script:阻塞 HTML 解析,无法操作后面的 DOM
  • body 底部:不阻塞页面渲染,DOM 已就绪,但下载时机晚
  • defer:并行下载,DOM 解析后按顺序执行,推荐用于主要脚本
  • async:并行下载,下载完立即执行,适合独立的第三方脚本
  • 最佳实践:使用 defer 放在 <head> 中,兼顾下载时机和执行时机