跳到正文
前端知识库
前端三大件

JavaScript 加载机制:同步/异步加载与页面渲染阻塞

理解 JavaScript 的加载机制是前端性能优化的基础。浏览器在解析 HTML 时,遇到 <script> 标签会有不同的处理策略,这直接影响页面的加载速度和用户体验。

约 3 分钟

JavaScript 加载机制:同步/异步加载与页面渲染阻塞

问题解析

理解 JavaScript 的加载机制是前端性能优化的基础。浏览器在解析 HTML 时,遇到 <script> 标签会有不同的处理策略,这直接影响页面的加载速度和用户体验。

核心概念

浏览器渲染流程

浏览器加载页面的基本流程:

下载 HTML → 解析 HTML(构建 DOM) → 下载 CSS → 构建 CSSOM → 合并成渲染树 → 布局 → 绘制

关键点:HTML 解析和 JS 执行共用同一个主线程,因此 JS 的加载和执行会影响 HTML 的解析进度。


同步加载(默认行为)

工作方式

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

浏览器遇到普通 <script> 标签时,执行以下步骤:

  1. 暂停 HTML 解析
  2. 下载 JS 文件(网络请求)
  3. 执行 JS 代码
  4. 恢复 HTML 解析

时序图

HTML 解析  ████████░░░░░░░░░░░░████████████
                   ↑            ↑
               暂停解析        恢复解析
JS 下载            ████████
JS 执行                    ████

为什么会阻塞?

JS 可以操作 DOM(如 document.write()),如果 JS 执行时 DOM 还在继续构建,会产生竞争问题,所以浏览器必须等待 JS 执行完毕再继续解析 HTML。


异步加载

async —— 异步加载,加载完立即执行

<script async src="script.js"></script>
阶段 行为
下载 与 HTML 解析并行进行,不阻塞
执行 下载完成后立即执行,此时暂停 HTML 解析
HTML 解析  ████████████░░░████████████████
JS  下载       ████████
JS  执行               ████

特点:

  • 执行顺序不确定(谁先下载完谁先执行)
  • 适合无依赖的独立脚本,如广告、统计埋点

defer —— 异步加载,延迟执行

<script defer src="script.js"></script>
阶段 行为
下载 与 HTML 解析并行进行,不阻塞
执行 等 HTML 完全解析完成后,按顺序执行
HTML 解析  ████████████████████████████
JS  下载       ████████
JS  执行                             ████  ← DOMContentLoaded 之前

特点:

  • 执行顺序按声明顺序保证
  • 适合有依赖关系的脚本(如 A 依赖 B)

三种方式对比

特性 普通 <script> async defer
HTML 解析是否阻塞(下载时) 是 否 否
HTML 解析是否阻塞(执行时) 是 是 否
执行时机 立即(下载后) 立即(下载后) HTML 解析完成后
执行顺序 按声明顺序 不确定 按声明顺序
适用场景 依赖 DOM 的内联脚本 独立无依赖脚本 依赖 DOM 的外部脚本

JS 加载是否阻塞页面渲染?

结论

加载方式 阻塞 HTML 解析 阻塞渲染
<script> 是 是
<script async> 执行时短暂阻塞 执行时短暂阻塞
<script defer> 否 否
<script> 放在 </body> 前 实际不阻塞(DOM 已解析) 近似否

深入理解

渲染阻塞 分两层含义:

  1. 阻塞 HTML 解析:浏览器停止构建 DOM 树
  2. 阻塞首次渲染(FCP):用户看到空白页面

同步 <script> 放在 <head> 中时,两者都会阻塞,直到脚本执行完毕页面才渲染。

<!-- 糟糕的实践:阻塞渲染 -->
<head>
  <script src="heavy-script.js"></script>  <!-- 用户看到白屏直到这里执行完 -->
</head>
<!-- 推荐实践 -->
<head>
  <script defer src="app.js"></script>  <!-- 不阻塞,HTML 解析完后执行 -->
</head>

动态创建脚本

JS 也可以动态插入 <script> 标签:

const script = document.createElement('script')
script.src = 'script.js'
script.async = true  // 默认就是 async = true
document.head.appendChild(script)

动态创建的脚本默认是异步的(相当于 async),不会阻塞页面解析。


最佳实践总结

<!DOCTYPE html>
<html>
<head>
  <!-- 1. 无依赖的第三方脚本用 async -->
  <script async src="analytics.js"></script>

  <!-- 2. 有依赖的业务脚本用 defer,保证顺序 -->
  <script defer src="lib.js"></script>
  <script defer src="app.js"></script>  <!-- 在 lib.js 之后执行 -->
</head>
<body>
  <!-- 3. 内联关键脚本放 </body> 前(兜底方案) -->
  <script>
    // 此时 DOM 已解析完毕
  </script>
</body>
</html>

优先级建议:defer > 放 </body> 前 > async > 默认同步


常见面试题

Q:DOMContentLoaded 和 load 有什么区别?

  • DOMContentLoaded:HTML 解析完成,defer 脚本执行完毕后触发,不等图片/CSS(深入阅读:三种方式对比)
  • load:页面所有资源(图片、CSS、JS)全部加载完成后触发

Q:async 和 defer 的 script 都不阻塞渲染,有什么区别?

核心区别在执行时机:async 下载完就执行(可能打断 HTML 解析),defer 等 HTML 完全解析后才执行(绝不打断)。多个 defer 按顺序执行,多个 async 顺序不定。(深入阅读:defer —— 异步加载,延迟执行)