脚本加载的基本机制
当浏览器遇到 <script> 标签时,默认行为是:暂停 HTML 解析 → 下载脚本 → 执行脚本 → 继续解析。这就是为什么把脚本放在 </body> 前曾是最佳实践。
| 加载方式 | 阻塞解析 | 执行时机 | 执行顺序 |
|---|---|---|---|
| 普通 | 是 | 下载后立即 | 按出现顺序 |
| async | 否 | 下载后立即 | 不确定 |
| defer | 否 | DOM 解析完成后 | 按出现顺序 |
| 动态插入 | 否 | 下载后立即 | 不确定 |
async vs defer
defer 属性
defer 脚本会在 HTML 解析完成后、DOMContentLoaded 事件前按顺序执行:
<!-- 推荐用于有依赖关系的脚本 -->
<script defer src="/js/vendor.js"></script>
<script defer src="/js/app.js"></script> <!-- 依赖 vendor.js -->
关键说明: defer 保证执行顺序,所以当 app.js 依赖 vendor.js 时,使用 defer 是安全的。
async 属性
async 脚本下载完成后立即执行,不保证顺序:
<!-- 适用于独立脚本,如统计代码 -->
<script async src="https://www.googletagmanager.com/gtag/js"></script>
选择建议
// 使用 defer 的场景
// - 需要操作 DOM
// - 脚本之间有依赖关系
// - 主应用代码
// 使用 async 的场景
// - 第三方统计、广告脚本
// - 完全独立的功能模块
// - 对执行时机不敏感的脚本
动态脚本加载
基础实现
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.async = true
script.onload = () => resolve()
script.onerror = () => reject(new Error(`Failed to load: ${src}`))
document.head.appendChild(script)
})
}
// 按需加载
button.addEventListener('click', async () => {
await loadScript('/js/heavy-feature.js')
heavyFeature.init()
})
带依赖的加载
async function loadWithDependencies(scripts: string[]) {
for (const src of scripts) {
await loadScript(src) // 顺序加载,保证依赖
}
}
// 使用
await loadWithDependencies([
'/js/jquery.min.js',
'/js/jquery-plugin.js' // 依赖 jQuery
])
模块化加载
ES Modules
现代浏览器原生支持 ES 模块,自带 defer 行为:
<script type="module" src="/js/app.mjs"></script>
模块脚本的特点:
- 默认 defer,不阻塞解析
- 自动严格模式
- 只执行一次(去重)
- 支持
import/export
动态导入
// 路由级代码分割
const routes = {
'/dashboard': () => import('./pages/Dashboard.vue'),
'/settings': () => import('./pages/Settings.vue')
}
// 按需加载
async function navigate(path: string) {
const loader = routes[path]
if (loader) {
const module = await loader()
renderComponent(module.default)
}
}
预加载与预取
preload - 高优先级加载
<!-- 告诉浏览器立即需要此资源 -->
<link rel="preload" href="/js/critical.js" as="script">
prefetch - 低优先级预取
<!-- 告诉浏览器将来可能需要此资源 -->
<link rel="prefetch" href="/js/next-page.js">
modulepreload
<!-- 预加载 ES 模块及其依赖 -->
<link rel="modulepreload" href="/js/app.mjs">
性能优化策略
关键脚本内联
对于首屏关键代码,考虑内联以减少请求:
<script>
// 关键的首屏逻辑(保持精简)
document.documentElement.classList.remove('no-js')
</script>
第三方脚本优化
<!-- 延迟加载非关键第三方脚本 -->
<script>
window.addEventListener('load', () => {
const script = document.createElement('script')
script.src = 'https://analytics.example.com/tracker.js'
document.body.appendChild(script)
})
</script>
使用 requestIdleCallback
// 在浏览器空闲时加载非关键脚本
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
loadScript('/js/non-critical.js')
})
} else {
setTimeout(() => {
loadScript('/js/non-critical.js')
}, 2000)
}
最佳实践总结
- 首屏关键脚本:内联或使用 preload
- 主应用代码:使用 defer 或 type="module"
- 独立功能:使用 async
- 第三方脚本:延迟加载,在 load 事件后
- 大型功能:动态导入,按需加载
正确的脚本加载策略可以显著改善页面加载性能,提升用户体验。


