script不加defer或async会阻塞HTML解析,导致白屏和交互延迟;推荐用defer、async、type="module"或动态import()优化加载策略。

script 标签不加 defer 或 async 时,浏览器会阻塞 HTML 解析
HTML5 默认按顺序加载和执行 ,遇到内联或外部脚本时,必须等它下载、解析、执行完才能继续构建 DOM。这对首屏渲染影响极大,尤其当脚本放在 里时,用户可能长时间看到白屏。
常见现象:页面顶部有 ,但 里的按钮半天不响应、文字不显示。
- 除非脚本必须同步执行(如初始化全局配置、Polyfill 检测),否则不要放在
中且不加属性 -
defer适合依赖 DOM 结构但需按顺序执行的脚本(如模块化入口) -
async适合完全独立、无执行顺序要求的脚本(如统计埋点、广告 SDK) - 现代项目中,
type="module"脚本默认具有defer行为,无需额外声明
把 script 移到 body 底部仍不够 —— 需配合 defer 或 type="module"
仅把 放到 前,虽能避免阻塞 DOM 构建,但仍有风险:如果 JS 文件体积大或网络慢,DOMContentLoaded 事件仍会被延迟触发,影响交互就绪时间。
更稳妥的做法是保留底部位置,同时显式声明加载策略:
立即学习“前端免费学习笔记(深入)”;
... ...
-
defer脚本会并行下载,但严格按出现顺序执行,且保证在DOMContentLoaded前完成 -
type="module"不仅自带defer,还支持顶层await和静态导入分析,适合现代构建流程 - 避免混用:
async脚本不应放在底部——它会跳过顺序约束,可能比 DOM 构建还早执行,导致document.getElementById找不到元素
动态 import() 是真正按需加载的关键,不是靠 script 位置解决的
调整 位置只能优化初始加载路径,无法解决“某个按钮点击后才需要加载图表库”这类场景。这时候必须用运行时动态导入。
例如点击按钮后加载 ECharts:
button.addEventListener('click', async () => {
const { default: echarts } = await import('https://cdn.jsdelivr.net/npm/echarts@5.4.3/+esm');
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({ /* ... */ });
});- 动态
import()返回 Promise,可配合try/catch处理加载失败 - CDN 提供的 ES 模块地址需带
+esm后缀(如 jsDelivr),否则可能返回 CommonJS 格式,导致浏览器报错Cannot use import statement outside a module - Webpack/Vite 等构建工具会自动代码分割,但最终是否生效,取决于你是否真的用了
import(),而不是只靠script标签挪位置
兼容性兜底:IE11 及更老浏览器不支持 defer 在内联脚本上生效
虽然 IE10+ 支持 defer,但对内联脚本(即没有 src 的 )行为不可靠,部分版本会忽略 defer 并立即执行。
- 若需兼容 IE,内联初始化逻辑应改写为 DOMContentLoaded 监听,或直接移入外部文件再用
defer -
type="module"在 IE 中完全不识别,会静默跳过;需搭配nomodule回退方案:
真正容易被忽略的是:很多人以为把 script 放到底部就“优化完了”,却没检查它是否真的等 DOM 就绪才执行 —— 特别是第三方 SDK 文档常默认写 引入,照抄就会拖慢首屏。











