Appearance
优化 script 标签加载缓慢问题
要点速览
- 脚本阻塞本质:默认
script会暂停 HTML 解析与渲染。 - 选型根原则:按“是否依赖 DOM/其他脚本”选择
async或defer。 async适合独立脚本;defer适合大多数业务脚本(有依赖)。type="module"天生“延迟执行(类 defer)”,按顺序执行且支持依赖图。
快速上手
用一个最小页面观察 async、defer 与默认脚本的加载与执行时机:
html
<!DOCTYPE html>
<html>
<head>
<!-- 独立脚本:下载并行,谁先下完谁先执行(会打断解析) -->
<script src="/js/a.js" async></script>
<!-- 业务脚本:下载并行,等解析完成后按书写顺序执行 -->
<script src="/js/b.js" defer></script>
<!-- 默认脚本:遇到即阻塞解析与渲染,下载并执行后才继续 -->
<script>
console.log("inline start - 阻塞解析");
</script>
</head>
<body>
<h1 id="title">Hello</h1>
<script>
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
});
window.addEventListener("load", () => {
console.log("window.load");
});
</script>
</body>
</html>性能认知与前提
- 默认
script会阻塞 HTML 解析,影响首屏;async/defer能并行下载减轻阻塞。 async在下载完成会立即执行,可能打断解析,且不保证脚本间顺序;defer保序、等解析完成再执行。- 业务脚本如果依赖 DOM 或其他库,优先
defer;统计/广告等独立脚本使用async。
二、面试回答误区与核心原则
(一)常见误区
多数人回答“脚本优化”类问题时,仅罗列async“defer”等技术名词,缺乏对问题本质的分析,无法体现系统性思维,难以满足面试官对“解决问题能力”的考察需求。
(二)高分核心原则
面对性能优化问题,先定义问题根源,再针对性给出解决方案,而非直接堆砌技术方案。该原则可体现从“诊断”到“解决”的完整思路,证明不仅会用工具,更能从根源解决问题。
三、script 标签加载缓慢的问题根源
浏览器默认处理script标签的机制是阻塞渲染:
- 当浏览器解析 HTML 遇到
script标签时,会立即暂停 HTML 解析、CSS 渲染等所有工作; - 优先下载
script脚本文件,下载完成后立即执行脚本; - 脚本执行结束后,才恢复 HTML 解析和页面渲染。
这一机制会导致页面出现“白屏”或“加载停滞”,是脚本引发页面加载缓慢的核心原因(注:字幕中“squid 标签”应为笔误,实际指script标签)。
拓展:模块脚本(type="module")的默认行为
type="module"脚本默认“延迟执行”(不阻塞解析,等 HTML 解析完成后执行),且脚本之间按依赖图与书写顺序执行。- 旧浏览器可结合
nomodule提供降级:现代浏览器加载模块脚本,旧浏览器加载非模块脚本。
四、优化方案:基于脚本依赖关系选择策略
优化script标签加载的唯一判断标准是:脚本是否依赖 DOM 或其他脚本。需根据依赖情况,选择不同属性进行优化,具体对比如下:
| 优化属性 | 适用场景 | 核心特性 | 注意事项 |
|---|---|---|---|
async | 脚本完全独立(不依赖 DOM、不依赖其他脚本),如: - 广告脚本 - 统计分析脚本(如埋点脚本) | 1. 下载阶段:与 HTML 解析并行进行,不阻塞渲染; 2. 执行阶段:脚本下载完成后立即执行(会暂停当前 HTML 解析); 3. 执行顺序:不保证脚本间的执行顺序(谁先下载完谁先执行) | 滥用会导致执行顺序混乱,引发偶发性 bug(如 A 脚本依赖 B 脚本,但 A 先执行),仅适用于无依赖的独立脚本 |
defer | 绝大多数业务脚本(依赖 DOM、依赖其他脚本),如: - 页面交互逻辑脚本 - 依赖 jQuery 的业务脚本 | 1. 下载阶段:与 HTML 解析并行进行,不阻塞渲染; 2. 执行阶段:脚本下载完成后,等待 HTML 解析完全结束再执行; 3. 执行顺序:严格按照脚本在 HTML 中的书写顺序执行 | 兼顾“不阻塞渲染”和“保证依赖关系”,是业务脚本的最优选择,无明显使用风险 |
场景选型建议
- 统计/广告/监控等独立脚本 →
async(并行下载,尽快上报,不依赖 DOM)。 - 应用主包、依赖 DOM 与第三方库 →
defer(并行下载,保序,解析后执行)。 - 现代前端入口(ESM) →
type="module"(天然延迟,支持 import 依赖图)。
五、面试回答框架(示例)
当面试官问“页面加载慢,脚本是主要原因,该怎么优化?”时,可按以下框架回答,体现完整思路:
先定位问题根源:
“首先,脚本导致页面加载慢的核心是浏览器默认处理script标签的阻塞机制——遇到script会暂停 HTML 解析和渲染,优先下载执行脚本,导致页面白屏或停滞。”再说明判断标准与优化策略:
“优化的关键是看脚本是否有依赖:
- 如果脚本完全独立(如广告、统计脚本),用
async属性,让脚本并行下载、下载完立即执行,不阻塞初始渲染; - 如果脚本依赖 DOM 或其他脚本(如业务逻辑脚本),用
defer属性,既保证脚本并行下载不阻塞渲染,又能在 HTML 解析完后按顺序执行,确保依赖关系正确。”
- 最后总结思路:
“核心是‘先看依赖,再定策略’,避免盲目使用属性导致新问题,从根源解决脚本阻塞渲染的问题。”
六、实践建议
- 业务脚本优先用
defer:多数业务脚本存在依赖(如依赖 DOM 加载完成、依赖其他库),defer能兼顾性能与稳定性,是更安全的选择; - 独立脚本谨慎用
async:仅对无任何依赖的脚本使用async,并在代码注释中明确标注“无依赖,可 async 加载”,避免后续维护时误改引发 bug; - 避免默认
script标签:除特殊场景(如脚本必须在页面头部执行且无替代方案),尽量不使用无属性的默认script标签,减少渲染阻塞。
常见误区
- 误把
async用在有依赖的业务脚本,导致执行顺序混乱与偶发现象。 - 假设
defer能解决所有问题,却忽略脚本自身的网络瓶颈(DNS、TLS、连接复用)。 - 将脚本“堆到页面底部”替代
defer,虽然减少阻塞,但无法保证脚本顺序与依赖就绪。
进一步优化(按需选用)
- 对第三方域名使用
dns-prefetch与preconnect(减少 DNS 与握手延迟):html<link rel="dns-prefetch" href="//cdn.example.com" /> <link rel="preconnect" href="https://cdn.example.com" crossorigin /> - 关键脚本可结合
preload(提前调度下载,但不会自动执行):html<link rel="preload" as="script" href="/js/app.js" /> <script src="/js/app.js" defer></script> - 长期建议:整合打包与分包策略(路由/功能拆分),减少初始脚本体积与阻塞面。
小结与后续
- 先诊断“是否阻塞解析”与“是否存在依赖”,再选择
async/defer/module。 - 业务脚本优先
defer,独立脚本用async;ESM 入口用type="module"。 - 配合网络层优化(
dns-prefetch/preconnect/preload)与打包拆分,进一步降低首屏压力。
