还记得第一次把个人博客部署到线上时的那种兴奋吗?我兴冲冲地把地址甩到群里,结果三秒后就有小伙伴吐槽:"你这网站是拨号上网时代的遗物吗?加载个按钮都要转三圈半。"那一刻,我恨不得把电脑塞进时光机送回1998年。
其实很多时候,网页加载慢并不是因为你的代码写得有多烂(虽然也不排除这种可能),而是因为那些看似无害的<script>标签在暗地里搞事情。它们就像一群不请自来的亲戚,非得在主人(HTML解析器)忙得不可开交的时候插嘴,把整个家(页面加载流程)搅得鸡飞狗跳。
让我们先做个小实验。假设你有这么个页面:
<!DOCTYPE html>
<html>
<head>
<title>我的超酷网站</title>
<script src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script>
</head>
<body>
<h1>欢迎光临</h1>
<p>这句话要等到上面那个2.8KB的lodash加载完才能看到</p>
</body>
</html>
浏览器看到这个页面时的内心OS大概是这样的:
“好的,开始解析HTML…哦,这里有个script标签!等等,我得先去下载这个lodash.js,下载完了还要执行它。什么?你说后面还有内容?不好意思,我得等这个大爷伺候完了再说。”
这就是script标签的默认行为——完全阻塞。它不仅会暂停HTML解析,还会暂停其他资源的下载。就像一个霸道的出租车司机,非得等到自己吃饱喝足了才肯发车。
让我们用一段代码来重现这个"惨案":
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>阻塞演示</title>
</head>
<body>
<div id="content"></div>
<!-- 故意放一个超大脚本 -->
<script>
// 模拟一个耗时5秒的计算
const start = Date.now();
while(Date.now() - start < 5000) {
// 啥也不干,就是硬等
}
document.getElementById('content').innerHTML = '终于加载完了!';
</script>
<!-- 这个div要5秒后才会出现 -->
<div>我是无辜的路人甲,为什么要陪我等?</div>
</body>
</html>
打开这个页面,你会发现整个页面空白了5秒,然后所有内容才"唰"地一下出现。这就是阻塞的代价——用户看到的不是渐进增强的页面,而是一个尴尬的白屏。
async和defer就像是给script标签请的两个保镖,专门负责管教那些不听话的脚本。它们的核心理念很简单:让脚本下载不耽误正事(HTML解析),至于执行时机,各有一套规矩。
早期的网页很简单,几个脚本标签,一些内联代码,大家排队执行,相安无事。但随着网页越来越复杂,脚本越来越多,这种"排队打饭"的模式就显得效率低下了。
于是浏览器厂商们开始琢磨:能不能让脚本在后台偷偷下载,别耽误HTML解析的正事?这就是async和defer的由来。它们就像是脚本界的"特殊通道",让不同类型的脚本能够更高效地加载。
async就像是个急性子的快递员,包裹(脚本)一到就立马派送(执行),完全不管你现在是不是在忙。让我们看个实际的例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>async 演示</title>
</head>
<body>
<h1>页面开始加载</h1>
<div id="log"></div>
<script>
// 记录日志的函数
function log(msg) {
const div = document.getElementById('log');
div.innerHTML += `<p>${new Date().toLocaleTimeString()}: ${msg}</p>`;
}
log('HTML 解析开始');
</script>
<!-- 第一个 async 脚本 -->
<script async src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"
onload="log('Lodash 加载完成')"></script>
<script>
log('继续解析 HTML');
</script>
<!-- 第二个 async 脚本 -->
<script async src="https://cdn.jsdelivr.net/npm/moment@2/moment.min.js"
onload="log('Moment 加载完成')"></script>
<script>
log('HTML 解析完成');
</script>
</body>
</html>
在这个例子中,你会发现:
async特别适合那些不依赖其他脚本,也不被其他脚本依赖的独立模块。比如:
<!-- 广告代码 - 完美的 async 候选人 -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<!-- 统计代码 - 另一个 async 爱好者 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<!-- 独立的小工具 - 也很适合 -->
<script async>
// 一个简单的返回顶部按钮,不需要任何依赖
window.addEventListener('load', function() {
const btn = document.createElement('button');
btn.innerHTML = '↑';
btn.style.cssText = 'position:fixed;bottom:20px;right:20px;';
btn.onclick = () => window.scrollTo({top: 0, behavior: 'smooth'});
document.body.appendChild(btn);
});
</script>
defer就像是剧院里彬彬有礼的观众,等到所有演员(HTML)都就位了,才按票号(出现顺序)依次上场。让我们见证一下它的优雅:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>defer 演示</title>
<!-- 三个 defer 脚本,注意顺序 -->
<script defer>
console.log('defer 脚本 1: 我在 DOMContentLoaded 之前执行');
console.log('此时 DOM 是否加载完成?', document.readyState);
</script>
<script defer src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script>
<script defer>
console.log('defer 脚本 2: 我在 lodash 之后执行,因为我在后面');
console.log('lodash 可用吗?', typeof _ !== 'undefined');
</script>
</head>
<body>
<h1>页面内容</h1>
<p>看控制台输出顺序</p>
<script>
// 这个普通脚本会先执行
console.log('普通脚本:我最先执行');
</script>
</body>
</html>
输出顺序总是:
defer的这种"按顺序延迟执行"的特性,让它成为了模块化代码的完美搭档。想象一下你在写一个复杂的单页应用:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>模块化加载演示</title>
<!-- 基础库,先加载 -->
<script defer src="js/utils.js"></script>
<!-- 依赖基础库的功能模块 -->
<script defer src="js/api.js"></script>
<script defer src="js/router.js"></script>
<!-- 最后加载主应用 -->
<script defer src="js/app.js"></script>
</head>
<body>
<div id="app">Loading...</div>
</body>
</html>
对应的模块代码:
// utils.js - 基础工具
window.MyApp = window.MyApp || {};
MyApp.utils = {
log: (msg) => console.log(`[MyApp] ${msg}`),
formatDate: (date) => date.toLocaleDateString()
};
// api.js - API 模块,依赖 utils
MyApp.api = {
fetchData: async function(endpoint) {
MyApp.utils.log(`Fetching data from ${endpoint}`);
// 模拟 API 调用
return new Promise(resolve => {
setTimeout(() => resolve({data: 'ok'}), 100);
});
}
};
// router.js - 路由模块
MyApp.router = {
routes: {},
addRoute: function(path, handler) {
this.routes[path] = handler;
},
navigate: function(path) {
MyApp.utils.log(`Navigating to ${path}`);
if (this.routes[path]) {
this.routes[path]();
}
}
};
// app.js - 主应用,依赖所有前面的模块
document.addEventListener('DOMContentLoaded', function() {
MyApp.utils.log('应用启动中...');
// 配置路由
MyApp.router.addRoute('/', () => {
document.getElementById('app').innerHTML = '<h1>首页</h1>';
});
MyApp.router.addRoute('/about', () => {
document.getElementById('app').innerHTML = '<h1>关于</h1>';
});
// 启动应用
MyApp.router.navigate('/');
});
由于defer保证了执行顺序,我们不用担心依赖问题。即使这些脚本在HTML解析完成前就已经下载好了,它们也会耐心地排队等待,按照正确的顺序执行。
让我们用一个综合示例来彻底搞清楚它们的区别:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>async vs defer 大对决</title>
<script>
// 记录各种事件的时间点
window.logTimes = {
scripts: [],
events: []
};
function addScriptLog(type, name) {
logTimes.scripts.push({
type: type,
name: name,
time: performance.now()
});
}
function addEventLog(event) {
logTimes.events.push({
event: event,
time: performance.now()
});
}
</script>
<!-- 各种脚本混用 -->
<script defer onload="addScriptLog('defer', 'defer1')">/* defer 1 */</script>
<script async onload="addScriptLog('async', 'async1')">/* async 1 */</script>
<script onload="addScriptLog('normal', 'normal1')">/* normal 1 */</script>
<script defer onload="addScriptLog('defer', 'defer2')">/* defer 2 */</script>
<script async onload="addScriptLog('async', 'async2')">/* async 2 */</script>
<script>
// 监听关键事件
document.addEventListener('DOMContentLoaded', () => addEventLog('DOMContentLoaded'));
window.addEventListener('load', () => addEventLog('load'));
// 页面加载完成后显示结果
window.addEventListener('load', function() {
setTimeout(() => {
console.table(logTimes.scripts);
console.table(logTimes.events);
// 分析结果
const analysis = `
执行顺序分析:
1. normal1 - 立即执行,阻塞解析
2. async1 或 async2 - 谁先下载完谁先执行
3. defer1 - DOMContentLoaded 前执行
4. defer2 - DOMContentLoaded 前执行(在 defer1 后)
事件触发:
- DOMContentLoaded:在 defer 脚本执行后触发
- load:所有资源加载完后触发
`;
console.log(analysis);
}, 100);
});
</script>
</head>
<body>
<h1>查看控制台输出</h1>
</body>
</html>
async和defer在处理依赖关系时的表现截然不同:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>依赖关系测试</title>
</head>
<body>
<h1>依赖关系演示</h1>
<!-- 场景1:async 脚本有依赖 -->
<h2>async 依赖测试(可能出错)</h2>
<script>
// 定义一个基础对象
window.MyLibrary = {
version: '1.0.0'
};
</script>
<!-- 这个 async 脚本依赖 MyLibrary -->
<script async>
// 这段代码可能会报错,因为执行时机不确定
if (window.MyLibrary) {
console.log('async: MyLibrary 版本是', MyLibrary.version);
} else {
console.error('async: MyLibrary 还没准备好!');
}
</script>
<!-- 场景2:defer 脚本有依赖 -->
<h2>defer 依赖测试(安全可靠)</h2>
<script>
// 定义另一个基础对象
window.AnotherLib = {
utils: {
format: (str) => str.toUpperCase()
}
};
</script>
<script defer>
// 这个一定能正常工作,因为 defer 保证了执行顺序
console.log('defer: 格式化结果', AnotherLib.utils.format('hello'));
</script>
<!-- 场景3:混合使用的复杂情况 -->
<h2>复杂依赖场景</h2>
<script>
// 主库
window.Framework = {
components: {},
register: function(name, component) {
this.components[name] = component;
}
};
</script>
<!-- 组件定义(用 defer,确保在 Framework 后执行) -->
<script defer>
Framework.register('button', {
template: '<button>Click me</button>'
});
</script>
<!-- 应用初始化(用 defer,确保在所有组件后执行) -->
<script defer>
document.addEventListener('DOMContentLoaded', function() {
console.log('可用组件:', Object.keys(Framework.components));
});
</script>
</body>
</html>
让我们深入研究这两种属性对关键事件的影响:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>事件时机测试</title>
<script>
const eventLog = [];
function logEvent(event, detail) {
const entry = {
time: performance.now().toFixed(2),
event: event,
detail: detail || ''
};
eventLog.push(entry);
console.table([entry]);
}
// 监听所有相关事件
document.addEventListener('readystatechange', () => {
logEvent('readystatechange', document.readyState);
});
document.addEventListener('DOMContentLoaded', () => {
logEvent('DOMContentLoaded');
});
window.addEventListener('load', () => {
logEvent('load');
});
</script>
<!-- 测试各种脚本对事件的影响 -->
<script defer
src="data:application/javascript,console.log('defer script executed')">
</script>
<script async
src="data:application/javascript,console.log('async script executed')">
</script>
<script>
logEvent('inline script');
</script>
</head>
<body>
<h1>事件时机测试结果</h1>
<p>查看控制台了解详细事件流</p>
<script>
// 页面加载完成后显示总结
window.addEventListener('load', () => {
setTimeout(() => {
console.log('%c事件流总结', 'font-size: 16px; font-weight: bold;');
console.table(eventLog);
const summary = `
关键观察点:
1. defer 脚本在 DOMContentLoaded 之前执行
2. async 脚本可能在 DOMContentLoaded 之前或之后执行
3. 所有脚本都在 load 事件之前执行
4. readystatechange 会经历: loading -> interactive -> complete
`;
console.log(summary);
}, 100);
});
</script>
</body>
</html>
在实际项目中,不同类型的第三方代码有不同的最佳实践:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>第三方脚本最佳实践</title>
<!-- 广告代码 - 用 async,不阻塞页面展示 -->
<!-- Google AdSense -->
<script async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
onerror="handleAdError()">
</script>
<!-- 统计代码 - 用 async,数据收集不能影响主流程 -->
<!-- Google Analytics -->
<script async
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID">
</script>
<script async>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
<!-- 客服聊天 - 可以 defer,等页面稳定后再加载 -->
<script defer>
// 等页面加载完成后再初始化客服系统
window.addEventListener('load', function() {
// 加载客服脚本
const script = document.createElement('script');
script.src = 'https://customer-service-provider.com/chat.js';
document.body.appendChild(script);
});
</script>
<!-- A/B测试 - 用 async,快速生效 -->
<script async>
// 简单的 A/B 测试实现
(function() {
const variant = Math.random() > 0.5 ? 'A' : 'B';
localStorage.setItem('ab_variant', variant);
// 应用变体
if (variant === 'B') {
document.documentElement.classList.add('variant-b');
}
})();
</script>
</head>
<body>
<h1>第三方脚本集成示例</h1>
<!-- 广告位 -->
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-1234567890"
data-ad-slot="1234567890"
data-ad-format="auto">
</ins>
<script>
// 处理广告加载失败
function handleAdError() {
console.warn('广告加载失败,显示占位内容');
const adSlots = document.querySelectorAll('.adsbygoogle');
adSlots.forEach(slot => {
slot.innerHTML = '<p style="background:#f0f0f0;padding:20px;text-align:center;">广告位</p>';
});
}
// 初始化广告(如果还没自动初始化)
if (typeof adsbygoogle !== 'undefined') {
adsbygoogle.push({});
}
</script>
</body>
</html>
单页应用的脚本加载策略更加复杂,需要精心安排:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SPA 脚本加载策略</title>
<!-- 关键样式内联,避免闪屏 -->
<style>
/* 关键 CSS 直接内联 */
body { margin: 0; font-family: sans-serif; }
#app { min-height: 100vh; }
.loading { text-align: center; padding: 50px; }
</style>
<!-- 第一步:加载应用核心(用 defer,确保顺序) -->
<script defer src="js/core/polyfills.js"></script>
<script defer src="js/core/framework.js"></script>
<script defer src="js/core/router.js"></script>
<!-- 第二步:加载功能模块(用 defer) -->
<script defer src="js/modules/auth.js"></script>
<script defer src="js/modules/api.js"></script>
<script defer src="js/modules/store.js"></script>
<!-- 第三步:主应用入口(用 defer,确保所有依赖加载完) -->
<script defer src="js/app.js"></script>
<!-- 非关键功能可以 async 加载 -->
<script async src="js/features/analytics.js"></script>
<script async src="js/features/chat-widget.js"></script>
<!-- 预加载关键资源 -->
<link rel="preload" href="js/modules/api.js" as="script">
<link rel="preload" href="fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
</head>
<body>
<div id="app">
<div class="loading">
<h1>应用加载中...</h1>
<p>首次加载可能需要一些时间</p>
</div>
</div>
<script>
// 内联的关键脚本,立即执行
(function() {
// 显示加载进度
const loadingEl = document.querySelector('.loading');
const progressEl = document.createElement('div');
progressEl.innerHTML = '<progress id="loadProgress" value="0" max="100">0%</progress>';
loadingEl.appendChild(progressEl);
// 监听脚本加载进度
const scripts = document.querySelectorAll('script[src]');
let loaded = 0;
scripts.forEach(script => {
if (script.hasAttribute('defer') || script.hasAttribute('async')) {
script.addEventListener('load', () => {
loaded++;
const progress = (loaded / scripts.length) * 100;
document.getElementById('loadProgress').value = progress;
});
}
});
})();
</script>
</body>
</html>
对应的模块化代码结构:
// js/core/framework.js - 框架核心
window.MySPA = {
// 核心功能
createApp: function(config) {
return {
config: config,
mount: function(selector) {
console.log('挂载应用到:', selector);
}
};
},
// 工具函数
utils: {
fetch: function(url) {
return fetch(url).then(r => r.json());
}
}
};
// js/modules/api.js - API 模块
MySPA.modules = MySPA.modules || {};
MySPA.modules.api = {
baseURL: '/api',
request: function(endpoint, options) {
return fetch(this.baseURL + endpoint, options)
.then(response => {
if (!response.ok) throw new Error('API Error');
return response.json();
});
},
get: function(endpoint) {
return this.request(endpoint, {method: 'GET'});
},
post: function(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
};
// js/app.js - 主应用
document.addEventListener('DOMContentLoaded', function() {
// 创建应用实例
const app = MySPA.createApp({
title: '我的SPA应用',
version: '1.0.0'
});
// 初始化路由
if (MySPA.modules.router) {
MySPA.modules.router.init();
}
// 挂载应用
app.mount('#app');
// 隐藏加载界面
const loadingEl = document.querySelector('.loading');
if (loadingEl) {
loadingEl.style.opacity = '0';
setTimeout(() => loadingEl.remove(), 300);
}
});
当页面需要加载多个相互依赖的脚本时,需要更精细的策略:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>复杂依赖管理</title>
<!-- 策略1:分层加载 -->
<!-- 核心层:框架和工具库 -->
<script defer src="libs/core.js"></script>
<script defer src="libs/utils.js"></script>
<!-- 服务层:API 和数据处理 -->
<script defer src="services/api.js"></script>
<script defer src="services/cache.js"></script>
<!-- 表现层:UI 组件 -->
<script defer src="components/base.js"></script>
<script defer src="components/form.js"></script>
<script defer src="components/table.js"></script>
<!-- 应用层:业务逻辑 -->
<script defer src="app/main.js"></script>
<!-- 策略2:条件加载 -->
<script>
// 根据浏览器特性决定加载策略
const isModernBrowser = 'IntersectionObserver' in window;
if (isModernBrowser) {
// 现代浏览器用 defer 加载完整功能
document.write('<script defer src="app/modern.js"><\/script>');
} else {
// 旧浏览器用 async 加载兼容版本
document.write('<script async src="app/legacy.js"><\/script>');
}
// 根据页面类型加载不同模块
const pageType = document.body.dataset.pageType;
switch(pageType) {
case 'dashboard':
import('./modules/dashboard.js');
break;
case 'profile':
import('./modules/profile.js');
break;
default:
import('./modules/common.js');
}
</script>
<!-- 策略3:动态加载 -->
<script>
// 动态脚本加载器
class ScriptLoader {
constructor() {
this.cache = new Map();
}
load(src, options = {}) {
// 如果已经加载过,直接返回缓存
if (this.cache.has(src)) {
return this.cache.get(src);
}
// 创建加载 Promise
const loadPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
// 根据选项设置属性
if (options.async) script.async = true;
if (options.defer) script.defer = true;
if (options.module) script.type = 'module';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
this.cache.set(src, loadPromise);
return loadPromise;
}
// 批量加载
loadAll(scripts) {
return Promise.all(
scripts.map(({src, ...options}) => this.load(src, options))
);
}
// 按顺序加载
loadSequence(scripts) {
return scripts.reduce((promise, {src, ...options}) => {
return promise.then(() => this.load(src, options));
}, Promise.resolve());
}
}
// 使用示例
const loader = new ScriptLoader();
// 并行加载非关键模块
loader.loadAll([
{src: 'widgets/chat.js', async: true},
{src: 'widgets/social.js', async: true},
{src: 'widgets/analytics.js', async: true}
]);
// 按顺序加载关键模块
loader.loadSequence([
{src: 'core/config.js', defer: true},
{src: 'core/auth.js', defer: true},
{src: 'app/main.js', defer: true}
]);
</script>
</head>
<body>
<h1>复杂依赖管理示例</h1>
</body>
</html>
真实案例:某电商网站的购物车功能突然失效,排查发现是因为新加入的促销脚本使用了async,导致它可能在购物车脚本之前执行:
<!-- 错误的加载方式 -->
<script async src="promotion.js"></script> <!-- 新加的促销脚本 -->
<script defer src="shopping-cart.js"></script> <!-- 原有的购物车脚本 -->
<!--
promotion.js 的内容:
-->
<script async>
// 错误的假设:以为购物车一定已经初始化
if (window.ShoppingCart) {
ShoppingCart.applyPromotion('SUMMER2024');
} else {
// 这里会执行,因为 ShoppingCart 可能还没加载
console.error('购物车模块未就绪!');
}
</script>
<!--
shopping-cart.js 的内容:
-->
<script defer>
window.ShoppingCart = {
items: [],
applyPromotion: function(code) {
console.log('应用促销码:', code);
// 实际逻辑...
}
};
</script>
解决方案1:使用事件系统解耦
<script async>
// promotion.js - 使用事件系统
document.addEventListener('shoppingCartReady', function() {
ShoppingCart.applyPromotion('SUMMER2024');
});
// 或者轮询检查(不推荐,仅作示例)
const checkCart = setInterval(function() {
if (window.ShoppingCart) {
clearInterval(checkCart);
ShoppingCart.applyPromotion('SUMMER2024');
}
}, 100);
</script>
解决方案2:统一使用defer并按顺序排列
<!-- 正确的加载方式 -->
<script defer src="shopping-cart.js"></script>
<script defer src="promotion.js"></script>
<!--
promotion.js 现在可以安全地假设 ShoppingCart 已存在
-->
<script defer>
// 这里可以放心使用 ShoppingCart
ShoppingCart.applyPromotion('SUMMER2024');
</script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM 操作踩坑示例</title>
<!-- 错误示例:async 脚本尝试操作可能不存在的 DOM -->
<script async>
// 这个脚本可能在 DOM 还没解析完就执行
const header = document.getElementById('header');
header.style.backgroundColor = 'red'; // 可能报错:Cannot read property 'style' of null
</script>
</head>
<body>
<h1 id="header">页面标题</h1>
<script>
// 记录错误
window.addEventListener('error', function(e) {
console.error('捕获到错误:', e.message);
});
</script>
</body>
</html>
正确的处理方式:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM 操作正确示例</title>
<!-- 方案1:使用 defer 代替 async -->
<script defer>
const header = document.getElementById('header');
if (header) {
header.style.backgroundColor = 'red';
}
</script>
<!-- 方案2:使用 DOMContentLoaded 事件 -->
<script async>
document.addEventListener('DOMContentLoaded', function() {
const header = document.getElementById('header');
if (header) {
header.style.backgroundColor = 'blue';
}
});
</script>
<!-- 方案3:使用 MutationObserver 作为终极方案 -->
<script async>
// 等待特定元素出现
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
const observer = new MutationObserver(function(mutations, obs) {
const element = document.querySelector(selector);
if (element) {
callback(element);
obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 使用示例
waitForElement('#header', function(header) {
header.style.backgroundColor = 'green';
});
</script>
</head>
<body>
<h1 id="header">页面标题</h1>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>defer 位置问题</title>
<!-- 问题:defer 脚本放在 iframe 里 -->
<script defer>
console.log('这个 defer 脚本可能不会按预期执行');
</script>
</head>
<body>
<!-- 如果页面内容是通过 document.write 动态生成的,defer 可能失效 -->
<script>
document.write('<h1>动态生成的内容</h1>');
</script>
<!-- 解决方案:确保 defer 脚本是静态的 -->
<!-- 错误示例 -->
<script>
// 动态创建的 script 标签,defer 可能无效
const script = document.createElement('script');
script.defer = true;
script.textContent = 'console.log("动态 defer 脚本")';
document.head.appendChild(script);
</script>
<!-- 正确示例 -->
<script defer>
// 静态定义的 defer 脚本
console.log('这个一定会按正确时机执行');
</script>
</body>
</html>
defer 失效的常见原因和解决方案:
// 解决方案1:使用动态导入
async function loadModuleDynamically() {
try {
const module = await import('./modules/feature.js');
module.init();
} catch (error) {
console.error('模块加载失败:', error);
}
}
// 解决方案2:使用加载队列
class ScriptQueue {
constructor() {
this.queue = [];
this.loaded = new Set();
}
add(src, options = {}) {
this.queue.push({src, ...options});
return this;
}
async execute() {
for (const {src, async = false} of this.queue) {
await this.loadScript(src, async);
}
}
loadScript(src, async) {
return new Promise((resolve, reject) => {
if (this.loaded.has(src)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
if (async) script.async = true;
script.onload = () => {
this.loaded.add(src);
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
}
// 使用示例
const queue = new ScriptQueue();
queue
.add('js/core.js')
.add('js/features.js')
.add('js/app.js')
.execute()
.then(() => {
console.log('所有脚本加载完成');
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>性能分析演示</title>
<!-- 性能标记 -->
<script>
// 创建性能标记
performance.mark('script-loading-start');
// 监听各个阶段
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'resource' && entry.initiatorType === 'script') {
console.log('脚本加载性能:', {
名称: entry.name.split('/').pop(),
加载时间: entry.duration.toFixed(2) + 'ms',
开始时间: entry.startTime.toFixed(2) + 'ms',
大小: (entry.transferSize / 1024).toFixed(2) + 'KB'
});
}
}
});
observer.observe({entryTypes: ['resource']});
</script>
<!-- 各种脚本加载方式对比 -->
<script defer src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"
onload="performance.mark('lodash-loaded')"></script>
<script async src="https://cdn.jsdelivr.net/npm/moment@2/moment.min.js"
onload="performance.mark('moment-loaded')"></script>
<script defer>
// 所有脚本加载完成后分析
window.addEventListener('load', function() {
performance.mark('page-loaded');
// 测量各个阶段
performance.measure('总加载时间', 'script-loading-start', 'page-loaded');
performance.measure('Lodash加载时间', 'script-loading-start', 'lodash-loaded');
performance.measure('Moment加载时间', 'script-loading-start', 'moment-loaded');
// 输出结果
const measures = performance.getEntriesByType('measure');
console.table(measures.map(m => ({
名称: m.name,
持续时间: m.duration.toFixed(2) + 'ms'
})));
// 清理
performance.clearMarks();
performance.clearMeasures();
});
</script>
</head>
<body>
<h1>性能分析演示</h1>
<p>打开 Chrome DevTools 的 Performance 面板查看详细数据</p>
<button onclick="analyzePerformance()">手动分析性能</button>
<script>
function analyzePerformance() {
// 获取所有脚本资源
const scripts = performance.getEntriesByType('resource')
.filter(r => r.initiatorType === 'script');
// 分析加载策略效果
const analysis = scripts.map(script => {
const url = new URL(script.name);
return {
脚本: url.pathname.split('/').pop(),
策略: script.name.includes('async') ? 'async' :
script.name.includes('defer') ? 'defer' : 'normal',
加载时间: script.duration.toFixed(2) + 'ms',
阻塞时间: (script.responseEnd - script.requestStart).toFixed(2) + 'ms'
};
});
console.table(analysis);
// 提供优化建议
const slowScripts = scripts.filter(s => s.duration > 1000);
if (slowScripts.length > 0) {
console.warn('发现慢加载脚本:', slowScripts);
console.log('优化建议:');
console.log('1. 考虑使用 CDN 加速');
console.log('2. 检查是否可以拆分大脚本');
console.log('3. 考虑使用代码分割');
}
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>预加载优化演示</title>
<!-- 预加载关键脚本 -->
<link rel="preload" href="js/critical-feature.js" as="script">
<link rel="preload" href="js/app-core.js" as="script">
<!-- 预加载字体资源 -->
<link rel="preload" href="fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预连接第三方域名 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
<script>
// 智能预加载器
class ResourcePreloader {
constructor() {
this.preloaded = new Set();
this.priorityQueue = [];
}
// 预加载脚本
preloadScript(src, options = {}) {
if (this.preloaded.has(src)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'script';
link.href = src;
link.onload = () => {
this.preloaded.add(src);
resolve();
// 如果是高优先级,立即执行
if (options.immediate) {
this.executeScript(src);
}
};
link.onerror = reject;
document.head.appendChild(link);
});
}
// 执行预加载的脚本
executeScript(src) {
const script = document.createElement('script');
script.src = src;
script.async = false; // 保持顺序
document.head.appendChild(script);
}
// 批量预加载
async preloadBatch(scripts) {
// 按优先级排序
const sorted = scripts.sort((a, b) => (b.priority || 0) - (a.priority || 0));
// 并行预加载
const promises = sorted.map(script =>
this.preloadScript(script.src, script.options)
);
await Promise.all(promises);
}
}
// 使用示例
const preloader = new ResourcePreloader();
// 预加载关键资源
preloader.preloadBatch([
{src: 'js/framework.js', priority: 10, options: {immediate: true}},
{src: 'js/router.js', priority: 9},
{src: 'js/components.js', priority: 8},
{src: 'js/analytics.js', priority: 3}
]).then(() => {
console.log('关键资源预加载完成');
});
</script>
</head>
<body>
<h1>预加载优化演示</h1>
<button onclick="testPreload()">测试预加载效果</button>
<script>
function testPreload() {
const perf = performance.getEntriesByType('resource');
const preloaded = perf.filter(r => r.initiatorType === 'link');
console.table(preloaded.map(r => ({
资源: r.name.split('/').pop(),
类型: r.initiatorType,
预加载时间: r.duration.toFixed(2) + 'ms'
})));
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>动态脚本控制</title>
<script>
// 高级脚本加载器
class AdvancedScriptLoader {
constructor(options = {}) {
this.options = {
defaultAsync: false,
defaultDefer: false,
timeout: 10000,
retryCount: 3,
...options
};
this.loading = new Map();
this.cache = new Map();
}
// 创建脚本元素
createScript(src, options = {}) {
const script = document.createElement('script');
script.src = src;
script.type = options.type || 'text/javascript';
// 设置 async 和 defer
if (options.async !== undefined) {
script.async = options.async;
} else if (this.options.defaultAsync) {
script.async = true;
}
if (options.defer !== undefined) {
script.defer = options.defer;
} else if (this.options.defaultDefer) {
script.defer = true;
}
// 添加自定义属性
if (options.dataset) {
Object.entries(options.dataset).forEach(([key, value]) => {
script.dataset[key] = value;
});
}
return script;
}
// 带重试机制的加载
async loadWithRetry(src, options = {}) {
const cacheKey = `${src}-${JSON.stringify(options)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
if (this.loading.has(cacheKey)) {
return this.loading.get(cacheKey);
}
const loadPromise = this.attemptLoad(src, options);
this.loading.set(cacheKey, loadPromise);
try {
const result = await loadPromise;
this.cache.set(cacheKey, result);
return result;
} catch (error) {
this.loading.delete(cacheKey);
throw error;
}
}
attemptLoad(src, options, attempt = 1) {
return new Promise((resolve, reject) => {
const script = this.createScript(src, options);
const timeout = setTimeout(() => {
reject(new Error(`Script load timeout: ${src}`));
}, this.options.timeout);
script.onload = () => {
clearTimeout(timeout);
resolve({
src,
loaded: true,
attempt
});
};
script.onerror = () => {
clearTimeout(timeout);
if (attempt < this.options.retryCount) {
console.warn(`Retry loading ${src}, attempt ${attempt + 1}`);
this.attemptLoad(src, options, attempt + 1)
.then(resolve)
.catch(reject);
} else {
reject(new Error(`Failed to load script after ${attempt} attempts: ${src}`));
}
};
document.head.appendChild(script);
});
}
// 并行加载多个脚本
async loadAll(scripts) {
const promises = scripts.map(script => {
if (typeof script === 'string') {
return this.loadWithRetry(script);
}
return this.loadWithRetry(script.src, script.options);
});
return Promise.all(promises);
}
// 按顺序加载脚本
async loadSequence(scripts) {
const results = [];
for (const script of scripts) {
if (typeof script === 'string') {
results.push(await this.loadWithRetry(script));
} else {
results.push(await this.loadWithRetry(script.src, script.options));
}
}
return results;
}
// 条件加载
async loadConditional(condition, src, options) {
if (typeof condition === 'function' ? condition() : condition) {
return this.loadWithRetry(src, options);
}
return null;
}
}
// 使用示例
const loader = new AdvancedScriptLoader({
defaultDefer: true,
timeout: 5000,
retryCount: 2
});
// 示例1:加载不同类型的脚本
async function loadDifferentScripts() {
try {
// 加载关键框架(defer,按顺序)
await loader.loadSequence([
{src: 'js/framework.js', options: {defer: true}},
{src: 'js/app.js', options: {defer: true}}
]);
// 加载第三方插件(async,并行)
await loader.loadAll([
{src: 'plugins/analytics.js', options: {async: true}},
{src: 'plugins/chat.js', options: {async: true}},
{src: 'plugins/social.js', options: {async: true}}
]);
console.log('所有脚本加载完成');
} catch (error) {
console.error('脚本加载失败:', error);
}
}
// 示例2:条件加载
async function loadFeatureFlags() {
// 只在需要时加载实验性功能
await loader.loadConditional(
() => localStorage.getItem('enableBetaFeatures') === 'true',
'js/beta-features.js',
{async: true}
);
// 根据浏览器特性加载 polyfill
await loader.loadConditional(
!window.IntersectionObserver,
'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver',
{defer: true}
);
}
// 示例3:错误恢复
async function loadWithFallback() {
const primaryLoader = loader.loadWithRetry('https://primary-cdn.com/app.js');
const fallbackLoader = loader.loadWithRetry('https://fallback-cdn.com/app.js');
try {
// 优先使用主 CDN
await primaryLoader;
} catch (error) {
console.warn('主 CDN 失败,使用备用 CDN');
// 主 CDN 失败时使用备用
await fallbackLoader;
}
}
</script>
</head>
<body>
<h1>动态脚本控制演示</h1>
<button onclick="loadDifferentScripts()">加载不同类型脚本</button>
<button onclick="loadFeatureFlags()">条件加载功能</button>
<button onclick="loadWithFallback()">错误恢复测试</button>
</body>
</html>
经过这一番深入浅出的探讨,相信你已经对async和defer这对孪生兄弟有了全新的认识。它们就像是你工具箱里的两把瑞士军刀——看起来相似,但各有各的拿手好戏。
记住这些金科玉律:
最后,送你一个万能模板,下次写页面直接照抄就行:
<!DOCTYPE html>
<html>
<head>
<!-- 关键样式内联 -->
<style>/* 关键CSS */</style>
<!-- 核心脚本用defer -->
<script defer src="js/framework.js"></script>
<script defer src="js/app.js"></script>
<!-- 第三方脚本用async -->
<script async src="https://third-party.com/analytics.js"></script>
<!-- 预加载关键资源 -->
<link rel="preload" href="js/critical.js" as="script">
</head>
<body>
<!-- 页面内容 -->
<!-- 动态功能最后加载 -->
<script>
// 页面加载完成后的初始化
window.addEventListener('load', function() {
// 非关键功能可以在这里动态加载
import('./features/nice-to-have.js');
});
</script>
</body>
</html>
下次再有人问你"async和defer到底有啥区别",别急着解释,直接把这篇文章甩给他,然后说:"自己看,看完请我喝奶茶!"毕竟,好知识就像好代码——值得分享,更值得用一杯奶茶的代价来交换。
记住,给你的script标签加上合适的属性,不仅是对浏览器负责,更是对用户最大的尊重。因为在这个快节奏的时代,用户的耐心就像IE浏览器的市场份额——越来越少啦!

