pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML defer与asyn - 详细教程与实战指南
教程简介
defer 和 async 是 <script> 标签的两个布尔属性,它们控制外部脚本的加载与执行时机,直接影响页面的渲染性能和脚本的行为。理解这两个属性的区别,以及它们与DOM解析的关系,是前端性能优化的关键知识。
本教程将深入讲解 defer 和 async 的工作原理、区别、适用场景、常见陷阱和性能优化策略。
核心概念
浏览器解析HTML的基本流程
在没有 defer 和 async 的情况下,浏览器遇到 <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兼容性
IE浏览器中的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>总结
defer 和 async 是优化脚本加载性能的两个关键属性,理解它们的区别和适用场景至关重要:
-
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使用不同策略。
本文涉及AI创作
内容由AI创作,请仔细甄别