pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML defer与asyn - 详细教程与实战指南

教程简介

deferasync<script> 标签的两个布尔属性,它们控制外部脚本的加载与执行时机,直接影响页面的渲染性能和脚本的行为。理解这两个属性的区别,以及它们与DOM解析的关系,是前端性能优化的关键知识。

本教程将深入讲解 defer 和 async 的工作原理、区别、适用场景、常见陷阱和性能优化策略。


核心概念

浏览器解析HTML的基本流程

在没有 deferasync 的情况下,浏览器遇到 <script> 标签时的行为:

  • 步骤1:浏览器开始解析HTML文档

  • 步骤2:遇到 <script src="app.js"> 标签

  • 步骤3暂停HTML解析

  • 步骤4:下载脚本文件

  • 步骤5:执行脚本

  • 步骤6恢复HTML解析

这个过程中,步骤3-5会阻塞HTML解析,导致页面渲染延迟,用户看到白屏时间更长。

defer的工作原理

defer 属性告诉浏览器:异步下载脚本,但延迟到HTML解析完成后再执行。

  • 步骤1:浏览器开始解析HTML文档

  • 步骤2:遇到 <script src="app.js" defer> 标签

  • 步骤3继续解析HTML,同时异步下载脚本

  • 步骤4:HTML解析完成

  • 步骤5按声明顺序执行所有defer脚本

  • 步骤6:触发 DOMContentLoaded 事件

async的工作原理

async 属性告诉浏览器:异步下载脚本,下载完成后立即执行。

  • 步骤1:浏览器开始解析HTML文档

  • 步骤2:遇到 <script src="app.js" async> 标签

  • 步骤3继续解析HTML,同时异步下载脚本

  • 步骤4:脚本下载完成,暂停HTML解析

  • 步骤5立即执行脚本

  • 步骤6恢复HTML解析

三种模式对比图

代码示例

无属性(同步):
HTML解析: ████████░░░░░░░░████████████████████
脚本下载:         ░░░░░░░░░░░░░░
脚本执行:                       ░░░░░░░░░░░░
DOMContentLoaded:                                        ▼

defer:
HTML解析: ████████████████████████████████████
脚本下载:   ░░░░░░░░░░░░░░░░
脚本执行:                                    ░░░░░░░░░░░░
DOMContentLoaded:                                              ▼

async:
HTML解析: ████████████████░░░░████████████████
脚本下载:   ░░░░░░░░░░░░
脚本执行:                 ░░░░░░░░░░
DOMContentLoaded:                                        ▼

语法与用法

基本语法

代码示例

<!-- defer 属性 -->
<script src="app.js" defer></script>

<!-- async 属性 -->
<script src="analytics.js" async></script>

defer的详细行为

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>defer行为演示</title>

  <!-- defer脚本按声明顺序执行 -->
  <script src="js/first.js" defer></script>
  <script src="js/second.js" defer></script>
  <script src="js/third.js" defer></script>
  <!-- 执行顺序保证:first -> second -> third -->

  <!-- defer脚本在DOMContentLoaded之前执行 -->
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      console.log('4. DOMContentLoaded 触发');
    });
    console.log('1. 内联脚本执行(同步)');
  </script>
</head>
<body>
  <h1>defer行为演示</h1>

  <script>
    console.log('2. body内联脚本执行');
  </script>
</body>
</html>

<!-- 执行顺序:
  1. 内联脚本执行(同步)
  2. body内联脚本执行
  3. first.js -> second.js -> third.js(defer脚本按顺序执行)
  4. DOMContentLoaded 触发
-->

async的详细行为

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>async行为演示</title>

  <!-- async脚本执行顺序不确定,取决于下载完成时间 -->
  <script src="js/slow-script.js" async></script>    <!-- 文件较大,下载慢 -->
  <script src="js/fast-script.js" async></script>    <!-- 文件较小,下载快 -->
  <!-- 可能的执行顺序:fast-script -> slow-script(谁先下载完谁先执行) -->
</head>
<body>
  <h1>async行为演示</h1>
</body>
</html>

同时使用defer和async

代码示例

<!-- 同时设置async和defer时,async优先 -->
<script src="app.js" async defer></script>
<!-- 等效于 <script src="app.js" async></script> -->

<!-- 如果浏览器不支持async,则回退到defer -->
<!-- 这是向后兼容的一种策略 -->

内联脚本的defer和async

代码示例

<!-- 内联脚本设置defer无效 -->
<script defer>
  console.log('defer对内联脚本无效,仍然同步执行');
</script>

<!-- 内联脚本设置async无效 -->
<script async>
  console.log('async对内联脚本无效,仍然同步执行');
</script>

<!-- 唯一例外:type="module"的内联脚本默认defer行为 -->
<script type="module">
  console.log('模块内联脚本有defer行为,在DOM解析后执行');
</script>

脚本加载与执行时机详解

完整的时机分析

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>脚本执行时机完整分析</title>

  <!-- 场景1:同步脚本(无属性) -->
  <script src="sync.js"></script>
  <!-- 行为:阻塞HTML解析,下载完成后执行,然后继续解析 -->

  <!-- 场景2:defer脚本 -->
  <script src="deferred.js" defer></script>
  <!-- 行为:异步下载,HTML解析完成后、DOMContentLoaded前执行 -->

  <!-- 场景3:async脚本 -->
  <script src="asynced.js" async></script>
  <!-- 行为:异步下载,下载完成后立即执行(可能阻塞HTML解析) -->

  <!-- 场景4:模块脚本 -->
  <script type="module" src="module.js"></script>
  <!-- 行为:默认defer行为 -->

  <!-- 场景5:带async的模块脚本 -->
  <script type="module" async src="module-async.js"></script>
  <!-- 行为:async行为,下载完成后立即执行 -->
</head>
<body>
  <h1>脚本执行时机完整分析</h1>

  <script>
    // 监听各种事件来理解执行时机
    console.log('[同步] body底部内联脚本');

    document.addEventListener('DOMContentLoaded', function() {
      console.log('[事件] DOMContentLoaded');
    });

    window.addEventListener('load', function() {
      console.log('[事件] window.load');
    });
  </script>
</body>
</html>

DOM解析与脚本执行的关系

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>DOM解析与脚本执行关系</title>

  <script>
    // 工具函数:记录当前DOM状态
    function logDomState(label) {
      var bodyChildren = document.body ? document.body.children.length : 0;
      console.log('[' + label + '] body子元素数量:' + bodyChildren +
        ' | readyState:' + document.readyState);
    }
  </script>

  <!-- 同步脚本:此时body可能还未解析 -->
  <script src="js/sync-lib.js"></script>

  <script>
    logDomState('同步脚本后');
  </script>

  <!-- defer脚本:在DOM解析完成后执行 -->
  <script src="js/deferred-app.js" defer></script>

  <!-- async脚本:下载完成后随时可能执行 -->
  <script src="js/async-analytics.js" async></script>
</head>
<body>
  <div id="content">页面内容</div>

  <script>
    logDomState('body底部脚本');

    document.addEventListener('DOMContentLoaded', function() {
      logDomState('DOMContentLoaded');
    });
  </script>
</body>
</html>

代码示例

示例1:defer脚本顺序保证

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>defer脚本顺序保证</title>

  <!-- 模拟多个有依赖关系的脚本 -->
  <script defer>
    // 内联defer无效,但我们可以用这种方式模拟
    window.__deferredScripts = window.__deferredScripts || [];
    window.__deferredScripts.push('inline-before-external');
    console.log('内联脚本(同步执行,defer无效)');
  </script>

  <!-- 外部defer脚本保证顺序 -->
  <script src="js/01-jquery.js" defer></script>
  <script src="js/02-jquery-plugin.js" defer></script>
  <script src="js/03-app-init.js" defer></script>

  <script>
    // 用DOMContentLoaded验证defer脚本已执行
    document.addEventListener('DOMContentLoaded', function() {
      var output = document.getElementById('output');
      if (typeof jQuery !== 'undefined') {
        output.innerHTML += '<p>jQuery 已加载,版本:' + jQuery.fn.jquery + '</p>';
      }
      if (typeof jQuery && jQuery.fn.pluginName) {
        output.innerHTML += '<p>jQuery 插件已加载</p>';
      }
      output.innerHTML += '<p>应用初始化完成</p>';
      output.innerHTML += '<p>DOMContentLoaded 事件触发</p>';
    });
  </script>
</head>
<body>
  <h1>defer脚本顺序保证</h1>
  <div id="output"></div>
</body>
</html>

示例2:async脚本独立性验证

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>async脚本独立性验证</title>

  <!-- 多个独立的async脚本 -->
  <script async>
    // 模拟独立的分析脚本
    window.__analytics = window.__analytics || {};
    window.__analytics.pageView = {
      timestamp: Date.now(),
      url: window.location.href
    };
    console.log('[Analytics] 页面浏览记录已发送');
  </script>

  <script async>
    // 模拟独立的A/B测试脚本
    window.__abTest = {
      variant: Math.random() > 0.5 ? 'A' : 'B',
      timestamp: Date.now()
    };
    console.log('[A/B Test] 变体:' + window.__abTest.variant);
  </script>

  <script async>
    // 模拟独立的错误监控脚本
    window.__errorMonitor = {
      errors: [],
      init: function() {
        window.addEventListener('error', function(e) {
          this.errors.push({
            message: e.message,
            filename: e.filename,
            lineno: e.lineno,
            timestamp: Date.now()
          });
        }.bind(this));
        console.log('[Error Monitor] 错误监控已启动');
      }
    };
    window.__errorMonitor.init();
  </script>
</head>
<body>
  <h1>async脚本独立性验证</h1>
  <p>这些async脚本互不依赖,执行顺序不影响功能</p>
  <div id="output"></div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      var output = document.getElementById('output');
      setTimeout(function() {
        output.innerHTML =
          '<p>分析数据:' + JSON.stringify(window.__analytics) + '</p>' +
          '<p>A/B测试:' + JSON.stringify(window.__abTest) + '</p>' +
          '<p>错误监控:' + (window.__errorMonitor ? '已启动' : '未启动') + '</p>';
      }, 100);
    });
  </script>
</body>
</html>

示例3:defer与async性能对比

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>defer与async性能对比</title>

  <style>
    .metric { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
    .metric-label { font-weight: bold; }
    .metric-value { color: #0066cc; }
    .comparison { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin: 20px 0; }
    .comparison-card { padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
    .comparison-card h3 { margin-top: 0; }
    .tag { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 12px; }
    .tag-sync { background: #ffcccc; color: #cc0000; }
    .tag-defer { background: #ccffcc; color: #006600; }
    .tag-async { background: #ccccff; color: #0000cc; }
  </style>
</head>
<body>
  <h1>defer与async性能对比</h1>

  <div class="comparison">
    <div class="comparison-card">
      <h3><span class="tag tag-sync">同步</span></h3>
      <ul>
        <li>阻塞HTML解析</li>
        <li>阻塞页面渲染</li>
        <li>按顺序执行</li>
        <li>首屏延迟最大</li>
      </ul>
    </div>
    <div class="comparison-card">
      <h3><span class="tag tag-defer">defer</span></h3>
      <ul>
        <li>不阻塞HTML解析</li>
        <li>不阻塞页面渲染</li>
        <li>按顺序执行</li>
        <li>首屏延迟最小</li>
      </ul>
    </div>
    <div class="comparison-card">
      <h3><span class="tag tag-async">async</span></h3>
      <ul>
        <li>不阻塞HTML解析(下载时)</li>
        <li>可能短暂阻塞渲染(执行时)</li>
        <li>执行顺序不确定</li>
        <li>首屏延迟中等</li>
      </ul>
    </div>
  </div>

  <h2>性能指标测量</h2>
  <div id="metrics"></div>

  <script>
    // 使用Performance API测量关键时间点
    window.addEventListener('load', function() {
      var timing = performance.timing;
      var metrics = document.getElementById('metrics');

      var data = {
        'DNS查询': timing.domainLookupEnd - timing.domainLookupStart,
        'TCP连接': timing.connectEnd - timing.connectStart,
        '请求发送': timing.responseStart - timing.requestStart,
        '响应接收': timing.responseEnd - timing.responseStart,
        'DOM解析': timing.domInteractive - timing.responseEnd,
        'DOM完成': timing.domComplete - timing.domInteractive,
        'DOMContentLoaded': timing.domContentLoadedEventEnd - timing.navigationStart,
        '页面完全加载': timing.loadEventEnd - timing.navigationStart
      };

      var html = '';
      for (var key in data) {
        html += '<div class="metric">' +
          '<span class="metric-label">' + key + ':</span>' +
          '<span class="metric-value">' + data[key] + 'ms</span>' +
          '</div>';
      }
      metrics.innerHTML = html;
    });
  </script>
</body>
</html>

示例4:混合使用defer和async

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>混合使用defer和async</title>

  <!-- 预连接CDN -->
  <link rel="preconnect" href="https://cdn.jsdelivr.net">

  <!-- 关键第三方库:defer,保证顺序 -->
  <script
    src="https://cdn.jsdelivr.net/npm/vue@3.3.8/dist/vue.global.prod.js"
    crossorigin="anonymous"
    defer>
  </script>

  <!-- 应用脚本:defer,依赖Vue -->
  <script src="/js/store.js" defer></script>
  <script src="/js/router.js" defer></script>
  <script src="/js/app.js" defer></script>

  <!-- 独立的分析脚本:async,不依赖其他脚本 -->
  <script src="/js/analytics.js" async></script>
  <script src="/js/error-tracking.js" async></script>
  <script src="/js/performance-monitor.js" async></script>
</head>
<body>
  <div id="app">
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
    <button @click="handleClick">点击</button>
    <p>点击次数:{{ count }}</p>
  </div>

  <script>
    // 等待Vue和app.js加载完成
    document.addEventListener('DOMContentLoaded', function() {
      // Vue应用初始化(由app.js处理)
      console.log('DOM解析完成,应用脚本应该已执行');
    });
  </script>
</body>
</html>

示例5:动态脚本的defer和async行为

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动态脚本的defer和async行为</title>
</head>
<body>
  <h1>动态脚本的defer和async行为</h1>
  <div id="output"></div>

  <script>
    var output = document.getElementById('output');

    function log(msg) {
      output.innerHTML += '<p>' + new Date().toLocaleTimeString() + ' - ' + msg + '</p>';
      console.log(msg);
    }

    // 动态创建的脚本默认async行为
    function createDynamicScript(src, options) {
      var script = document.createElement('script');
      script.src = src;

      if (options) {
        // 注意:动态脚本的async默认为true
        // 显式设置async=false可以实现类似defer的顺序执行
        if (options.async === false) {
          script.async = false; // 关键:保证顺序执行
        }
        if (options.defer) {
          script.defer = true;
        }
      }

      script.onload = function() {
        log('脚本加载完成:' + src);
      };

      script.onerror = function() {
        log('脚本加载失败:' + src);
      };

      return script;
    }

    // 示例1:动态脚本默认async行为
    log('创建动态脚本(默认async)');
    var script1 = createDynamicScript('/js/module-a.js');
    var script2 = createDynamicScript('/js/module-b.js');
    document.head.appendChild(script1);
    document.head.appendChild(script2);
    // 执行顺序不确定

    // 示例2:动态脚本设置async=false保证顺序
    log('创建动态脚本(async=false,保证顺序)');
    var script3 = createDynamicScript('/js/dep-a.js', { async: false });
    var script4 = createDynamicScript('/js/dep-b.js', { async: false });
    document.head.appendChild(script3);
    document.head.appendChild(script4);
    // 保证执行顺序:dep-a -> dep-b

    // 示例3:使用Promise链式加载
    function loadScriptSequential(urls) {
      return urls.reduce(function(promise, url) {
        return promise.then(function() {
          return new Promise(function(resolve, reject) {
            var script = document.createElement('script');
            script.src = url;
            script.async = false;
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
          });
        });
      }, Promise.resolve());
    }

    // 使用方式
    // loadScriptSequential(['/js/a.js', '/js/b.js', '/js/c.js']);
  </script>
</body>
</html>

浏览器兼容性

defer与async兼容性

特性 Chrome Firefox Safari Edge IE
defer 基本支持 全部 全部 全部 全部 4+
defer 顺序保证 全部 全部 全部 全部 10+(IE9有bug)
async 全部 全部 全部 全部 10+
defer + async 同时使用 全部 全部 全部 全部 10+
动态脚本默认async 全部 全部 全部 全部 不支持
动态脚本async=false 全部 全部 全部 全部 不支持
模块脚本默认defer 61+ 60+ 10.1+ 16+ 不支持

IE浏览器中的defer已知问题

问题 IE版本 描述
执行顺序不保证 IE4-IE9 多个defer脚本可能不按声明顺序执行
内联脚本与defer脚本混合 IE9- 内联脚本可能在defer脚本之前或之后执行
document.write IE9- defer脚本中的document.write可能覆盖整个页面
条件注释中的defer IE9- 条件注释中的defer脚本可能不执行

注意事项与最佳实践

1. 选择defer还是async的决策树

代码示例

脚本是否有依赖关系?
├── 是 → 使用 defer
│   ├── 依赖其他脚本 → defer(保证顺序)
│   └── 被其他脚本依赖 → defer(保证顺序)
└── 否 → 使用 async
    ├── 独立的分析/监控脚本 → async
    ├── 广告脚本 → async
    └── 社交分享按钮脚本 → async

如果不确定 → 使用 defer(更安全的选择)

2. defer的最佳实践

代码示例

<!-- 推荐:所有应用脚本使用defer -->
<head>
  <script src="/js/vendor.js" defer></script>
  <script src="/js/app.js" defer></script>
</head>

<!-- 推荐:defer脚本放在head中,尽早开始下载 -->
<head>
  <script src="/js/app.js" defer></script>
</head>

<!-- 不推荐:defer脚本放在body底部(浪费了并行下载的优势) -->
<body>
  <!-- 页面内容 -->
  <script src="/js/app.js" defer></script>
</body>

3. async的最佳实践

代码示例

<!-- 推荐:独立的第三方脚本使用async -->
<script src="https://www.google-analytics.com/analytics.js" async></script>
<script src="https://connect.facebook.net/sdk.js" async></script>

<!-- 不推荐:有依赖关系的脚本使用async -->
<script src="jquery.js" async></script>
<script src="jquery-plugin.js" async></script>  <!-- 可能在jquery之前执行 -->

<!-- 推荐:有依赖关系的脚本使用defer或合并 -->
<script src="bundle.js" async></script>

4. 避免的常见陷阱

代码示例

<!-- 陷阱1:混合使用同步和defer脚本 -->
<script src="sync-lib.js"></script>           <!-- 同步,阻塞解析 -->
<script src="deferred-app.js" defer></script>  <!-- defer,但被上面的同步脚本延迟 -->

<!-- 陷阱2:async脚本操作DOM -->
<script async>
  // async脚本执行时DOM可能还未解析完成
  document.getElementById('myElement').textContent = 'Hello'; // 可能报错
</script>

<!-- 陷阱3:依赖DOMContentLoaded的async脚本 -->
<script async>
  document.addEventListener('DOMContentLoaded', function() {
    // 如果async脚本在DOMContentLoaded之后才下载完成,这个回调不会执行
    console.log('可能永远不会执行');
  });
</script>

<!-- 陷阱4:动态脚本忘记设置async=false -->
<script>
  var script = document.createElement('script');
  script.src = '/js/app.js';
  // 动态脚本默认async=true,如果不希望这样需要显式设置
  script.async = false;  // 保证顺序执行
  document.head.appendChild(script);
</script>

代码规范示例

推荐的脚本加载策略

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>推荐的脚本加载策略</title>

  <!-- 1. 预连接CDN -->
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>

  <!-- 2. 预加载关键脚本 -->
  <link rel="preload" href="/js/app.abc123.js" as="script">

  <!-- 3. 第三方CDN库:defer + SRI -->
  <script
    src="https://cdn.jsdelivr.net/npm/vue@3.3.8/dist/vue.global.prod.js"
    integrity="sha384-xxxxx"
    crossorigin="anonymous"
    defer>
  </script>

  <!-- 4. 应用脚本:defer,按依赖顺序声明 -->
  <script src="/js/vendor.def456.js" defer></script>
  <script src="/js/app.abc123.js" defer></script>

  <!-- 5. 独立分析脚本:async -->
  <script src="/js/analytics.js" async></script>
  <script src="/js/error-tracking.js" async></script>
</head>
<body>
  <!-- 页面内容 -->

  <noscript>
    <p>请启用JavaScript以获得最佳体验。</p>
  </noscript>
</body>
</html>

常见问题与解决方案

问题1:defer脚本中DOMContentLoaded事件的时机

症状:在defer脚本中注册的DOMContentLoaded回调不执行。

代码示例

<!-- 问题代码 -->
<script src="app.js" defer></script>
<!-- app.js 内容:
  document.addEventListener('DOMContentLoaded', function() {
    console.log('这可能不会执行');
  });
-->

原因:defer脚本在DOMContentLoaded之前执行,但如果defer脚本注册DOMContentLoaded监听器时事件已经触发,则回调不会执行。

解决方案:

代码示例

// 方案1:直接执行,不需要等待DOMContentLoaded(defer脚本执行时DOM已解析完成)
console.log('直接操作DOM即可');

// 方案2:检查readyState
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init(); // DOM已就绪,直接执行
}

function init() {
  console.log('初始化应用');
}

问题2:async脚本执行时DOM未就绪

症状:async脚本中获取DOM元素返回null。

代码示例

<!-- 问题代码 -->
<script async src="app.js"></script>
<!-- app.js 内容:
  var el = document.getElementById('myElement'); // 可能为null
-->

解决方案:

代码示例

// 方案1:使用DOMContentLoaded(注意可能已触发)
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else if (document.readyState === 'interactive' || document.readyState === 'complete') {
  init();
}

function init() {
  var el = document.getElementById('myElement');
  if (el) {
    el.textContent = 'Hello';
  }
}

// 方案2:改用defer
// <script src="app.js" defer></script>

问题3:IE中defer脚本顺序错乱

症状:在IE9及以下版本中,多个defer脚本的执行顺序与声明顺序不一致。

解决方案:

代码示例

<!-- 方案1:合并为单个文件 -->
<script src="/js/bundle.js" defer></script>

<!-- 方案2:使用脚本加载器 -->
<script>
  (function() {
    var scripts = [
      '/js/jquery.js',
      '/js/plugin.js',
      '/js/app.js'
    ];

    function loadSequential(index) {
      if (index >= scripts.length) return;
      var script = document.createElement('script');
      script.src = scripts[index];
      script.onload = function() {
        loadSequential(index + 1);
      };
      document.head.appendChild(script);
    }

    loadSequential(0);
  })();
</script>

<!-- 方案3:条件注释针对IE使用不同策略 -->
<!--[if lt IE 10]>
  <script src="/js/bundle.js"></script>
<![endif]-->
<!--[if gte IE 10]><!-->
  <script src="/js/app.js" defer></script>
<!--<![endif]-->

问题4:async脚本与页面其他脚本冲突

症状:async脚本修改了全局变量,影响其他脚本的行为。

解决方案:

代码示例

<!-- 方案1:async脚本使用IIFE避免全局污染 -->
<script async>
  (function() {
    'use strict';
    // 所有变量都在IIFE作用域内
    var analyticsData = {};
    // 不会污染全局
  })();
</script>

<!-- 方案2:使用命名空间 -->
<script async>
  window.MyAnalytics = window.MyAnalytics || {};
  MyAnalytics.track = function(event) {
    // ...
  };
</script>

<!-- 方案3:使用ES6模块 -->
<script type="module" async>
  // 模块有自己的作用域
  const analyticsData = {};
  // 不会污染全局
</script>

总结

deferasync 是优化脚本加载性能的两个关键属性,理解它们的区别和适用场景至关重要:

  • defer:异步下载,HTML解析后按顺序执行,适合有依赖关系的应用脚本

  • async:异步下载,下载完立即执行,顺序不可控,适合独立的第三方脚本

  • 默认(无属性):同步下载执行,阻塞HTML解析,应尽量避免

  • 模块脚本:默认defer行为,可以显式设置async

  • 动态脚本:默认async行为,设置async=false可保证顺序

  • 最佳实践:应用脚本用defer,分析脚本用async,关键路径用preload

  • IE兼容:IE9及以下defer有bug,需要特殊处理

选择正确的脚本加载策略,能够显著改善页面的首屏渲染时间和用户体验。在大多数情况下,优先使用 defer 是最安全的选择。

常见问题

defer和async的核心区别是什么?

defer:异步下载脚本,但在DOM解析完成后按声明顺序执行,适合有依赖关系的脚本。async:异步下载脚本,下载完成后立即执行,执行顺序不可控,适合独立的脚本如分析代码。两者都不会阻塞HTML解析,但defer保证执行顺序,async不保证。

什么时候应该使用defer?

当脚本之间有依赖关系时使用defer,例如jQuery必须在jQuery插件之前执行。defer脚本按声明顺序执行,且在DOMContentLoaded事件之前执行。推荐将所有应用脚本都标记为defer,并放在head标签中以尽早开始下载。

什么时候应该使用async?

当脚本完全独立、不依赖其他脚本也不被其他脚本依赖时使用async。典型场景包括:Google Analytics分析脚本、广告脚本、社交分享按钮脚本等第三方独立脚本。async脚本下载完成后立即执行,可能短暂阻塞HTML解析。

为什么defer脚本推荐放在head中?

defer脚本不会阻塞HTML解析,所以放在head中可以让浏览器尽早开始下载脚本,与HTML解析并行进行。如果放在body底部,虽然不会阻塞解析,但浪费了与HTML解析并行下载的时间窗口。现代最佳实践是将defer脚本放在head中,尽早开始下载。

动态创建的脚本默认是什么行为?

动态创建的script元素默认async=true,即下载完成后立即执行,顺序不可控。如果需要保证顺序执行,必须显式设置script.async = false。这是HTML5规范的行为,与静态声明的脚本默认同步行为不同。使用Promise链式加载或设置async=false可以实现顺序加载。

IE浏览器中defer有什么已知问题?

IE9及以下版本中,多个defer脚本可能不按声明顺序执行。IE9中内联脚本可能在defer脚本之前或之后执行,行为不可预测。defer脚本中的document.write可能覆盖整个页面。解决方案包括:合并为单个文件、使用脚本加载器保证顺序、或使用条件注释针对IE使用不同策略。

标签: defer属性 async属性 脚本加载 性能优化 DOM解析 浏览器兼容

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML外部JavaScrip - 详细教程与实战指南 下一篇: JS集成:HTML DOM概述 - 详细教程与实战指南

poll相关推荐