脚本加载优化

defer、async 和动态创建 script 的区别与使用场景

问题

如何优化页面中 JavaScript 脚本的加载?deferasync 和动态创建 script 有什么区别?

解答

普通 script 加载

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

浏览器遇到 <script> 标签时会:

  1. 暂停 HTML 解析
  2. 下载脚本
  3. 执行脚本
  4. 继续解析 HTML

这会阻塞页面渲染。

defer

<script src="app.js" defer></script>
  • 下载与 HTML 解析并行
  • 在 HTML 解析完成后、DOMContentLoaded 事件前执行
  • 多个 defer 脚本按顺序执行
<!-- 按 1, 2, 3 顺序执行 -->
<script src="1.js" defer></script>
<script src="2.js" defer></script>
<script src="3.js" defer></script>

async

<script src="app.js" async></script>
  • 下载与 HTML 解析并行
  • 下载完成后立即执行(会暂停 HTML 解析)
  • 多个 async 脚本执行顺序不确定
<!-- 执行顺序取决于下载完成时间 -->
<script src="1.js" async></script>
<script src="2.js" async></script>
<script src="3.js" async></script>

动态创建 script

// 基本用法
function loadScript(src) {
  const script = document.createElement('script');
  script.src = src;
  document.body.appendChild(script);
}

// 带回调的版本
function loadScript(src, callback) {
  const script = document.createElement('script');
  script.src = src;
  
  script.onload = () => callback(null);
  script.onerror = () => callback(new Error(`Failed to load ${src}`));
  
  document.body.appendChild(script);
}

// Promise 版本
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

// 使用
loadScript('https://example.com/lib.js')
  .then(() => console.log('loaded'))
  .catch(() => console.log('failed'));

动态创建的 script 默认是 async 行为,可以手动设置:

const script = document.createElement('script');
script.src = 'app.js';
script.async = false; // 按插入顺序执行
document.body.appendChild(script);

加载时序对比

普通 script:
HTML: ──────█████████████──────────────────────▶
           ↑停止解析    ↑继续解析
JS:        ████下载████████执行████

defer:
HTML: ──────────────────────────────────────────▶
JS:        ████下载████              ████执行████
                                    ↑HTML解析完成后

async:
HTML: ──────────────────█████──────────────────▶
                       ↑暂停  ↑继续
JS:        ████下载████████执行████
                       ↑下载完立即执行

使用场景

<!-- 依赖 DOM 的脚本,需要保证顺序 -->
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>

<!-- 独立的第三方脚本,如统计、广告 -->
<script src="analytics.js" async></script>

<!-- 按需加载 -->
<script>
  button.onclick = () => {
    loadScript('heavy-feature.js');
  };
</script>

关键点

  • defer:并行下载,HTML 解析后按顺序执行,适合有依赖关系的脚本
  • async:并行下载,下载完立即执行,顺序不确定,适合独立脚本
  • 动态创建 script 默认是 async 行为,设置 async = false 可按顺序执行
  • deferasync 仅对外部脚本有效,内联脚本会忽略这两个属性
  • 现代项目通常把 <script> 放在 </body> 前,或使用 defer