在 JavaScript 中,脚本的加载时机直接影响页面的解析、渲染和交互体验。默认情况下,浏览器解析 HTML 时遇到<script>
标签会暂停 HTML 解析,优先下载并执行脚本(因为脚本可能会修改 DOM/CSSOM),这可能导致页面加载阻塞。
我们可以通过多种方式控制 JS 的加载时机,以优化页面性能和交互体验,具体如下:
一、默认加载行为(同步加载)
最基础的<script>
标签加载方式,会阻塞 HTML 解析:
js
<!-- 同步加载:遇到脚本会暂停HTML解析,下载并执行完成后才继续解析 -->
<script src="script.js"></script>
特点:
- 阻塞 HTML 解析和页面渲染,可能导致页面空白时间过长。
- 脚本执行时可以访问到当前已解析的 DOM(但后续 DOM 还未解析)。
二、控制加载时机的常用方式
1. 利用async
属性(异步加载 + 乱序执行)
async
让脚本异步下载 (不阻塞 HTML 解析),下载完成后立即执行(可能打断 HTML 解析):
js
<!-- 异步加载:下载时不阻塞解析,下载完立即执行(执行顺序不确定) -->
<script src="script1.js" async></script>
<script src="script2.js" async></script>
特点:
- 脚本下载和 HTML 解析并行进行,不阻塞初始解析。
- 执行顺序与下载完成时间有关(谁先下载完谁先执行),适合无依赖的独立脚本(如广告、统计代码)。
- 执行时可能 DOM 尚未完全解析,需注意访问 DOM 的时机。
2. 利用defer
属性(异步加载 + 顺序执行)
defer
让脚本异步下载 (不阻塞 HTML 解析),但延迟到 HTML 完全解析后执行,且保持脚本顺序:
js
<!-- 延迟执行:下载不阻塞解析,等HTML解析完后按顺序执行 -->
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
特点:
- 下载和 HTML 解析并行,不阻塞初始解析。
- 执行顺序与标签在 HTML 中的顺序一致(先 script1 后 script2),适合有依赖关系的脚本(如库 + 业务代码)。
- 执行时机在
DOMContentLoaded
事件之前(DOM 完全就绪后)。
3. 动态创建<script>
标签(按需异步加载)
通过 JavaScript 动态创建<script>
元素并插入 DOM,默认是异步加载(不阻塞解析)
html
<script>
// 动态创建脚本,按需加载
function loadScript(url, callback) {
const script = document.createElement('script');
script.src = url;
// 加载完成后执行回调
script.onload = callback;
// 插入到DOM中开始下载
document.body.appendChild(script);
}
// 用法:需要时再加载
loadScript('library.js', () => {
console.log('库加载完成,可以使用了');
});
</script>
特点:
- 完全由代码控制加载时机(如用户触发事件后加载),实现 "按需加载"。
- 默认异步加载,不阻塞 HTML 解析。
- 可通过
script.async = false
强制按插入顺序执行(类似 defer)。
4. 模块加载(type="module"
)
ES6 模块默认采用延迟加载(类似defer
),且支持模块化语法(import
/export
):
js
<!-- 模块加载:默认延迟执行,按顺序加载,支持import -->
<script type="module" src="module1.js"></script>
<script type="module" src="module2.js"></script>
特点:
- 下载时不阻塞 HTML 解析,执行时机在 HTML 解析完成后(类似 defer)。
- 按标签顺序执行,支持模块间依赖(通过
import
声明)。 - 模块内部默认是严格模式(
use strict
),且顶层变量不污染全局。 - 可通过
async
属性让模块下载完成后立即执行(忽略顺序)
js
<script type="module" src="module.js" async></script>
5. 放置位置控制(传统方式)
将脚本放在<body>
底部,让 HTML 解析完成后再加载执行:
js
<body>
<!-- 页面内容 -->
<div>...</div>
<!-- 放在body底部:HTML解析完后再加载执行 -->
<script src="script.js"></script>
</body>
特点:
- 避免阻塞 HTML 解析,脚本执行时可访问完整 DOM。
- 但下载仍会阻塞页面渲染(不如 async/defer 高效),现代开发更推荐前几种方式。
6. 监听页面就绪事件(控制执行时机)
即使脚本提前加载,也可通过事件监听延迟执行(确保 DOM 就绪):
js
<script src="script.js"></script>
<script>
// 等待DOM完全解析完成后执行
document.addEventListener('DOMContentLoaded', () => {
// 此时可安全操作DOM
initApp();
});
// 等待所有资源(图片、样式等)加载完成后执行
window.addEventListener('load', () => {
// 适合需要依赖资源的操作
startAnimation();
});
</script>
三、总结:不同场景的选择
需求场景 | 推荐方式 | 核心优势 |
---|---|---|
脚本无依赖,独立运行 | async 属性 |
下载最快,不阻塞解析 |
脚本有依赖,需按顺序执行 | defer 属性 |
顺序执行,DOM 就绪后执行 |
按需加载(如用户操作后) | 动态创建<script> 标签 |
灵活控制加载时机 |
现代模块化项目 | type="module" |
支持 import/export,延迟执行 |
兼容旧环境,确保 DOM 访问 | 放在<body> 底部 |
简单直接,兼容性好 |
合理控制 JS 加载时机,可以显著减少页面阻塞时间,提升用户体验(尤其是首屏加载速度)。