返回首页

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:DOMContentLoadedload 有什么区别?

  • DOMContentLoaded:HTML 解析完成,defer 脚本执行完毕后触发,不等图片/CSS
  • load:页面所有资源(图片、CSS、JS)全部加载完成后触发

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

核心区别在执行时机async 下载完就执行(可能打断 HTML 解析),defer 等 HTML 完全解析后才执行(绝不打断)。多个 defer 按顺序执行,多个 async 顺序不定。