本文目录
拆一箱施工材料时,工人一边读图纸,一边发现水管、门窗和电线。有些材料一到就能并行准备,有些工作必须等前一步完成。浏览器解析 HTML 也不是先把全文读完、再统一执行;它边读边构建 DOM,边发现资源,遇到脚本和样式时会改变节奏。
先猜:三个 script 谁先运行
<script src="normal.js"></script>
<script async src="analytics.js"></script>
<script defer src="page.js"></script>普通 script 通常会让 HTML 解析器在执行点暂停;async 脚本下载完成后尽快执行,可能打断解析,顺序不保证;defer 脚本并行下载,但通常等 HTML 解析完成后按文档顺序执行。三者不是“快、中、慢”三个速度等级,而是不同的阻塞和顺序契约。
HTML 解析和 DOM 构建
解析器一边构建 DOM,一边发现资源;“发现”与“完成下载”是两件事。
解析器把字符分成 token(标记),再根据标签关系构建 DOM(Document Object Model,文档对象模型)。看到 <img>、<link>、<script> 等标签时,解析器或预加载扫描器可能发现子资源,浏览器就可以开始调度下载。
console.log(document.readyState)
const title = document.querySelector('title')解析仍在进行时,DOM 可能只有部分节点;脚本如果在文档前部运行,后面的元素还不存在。把 document.querySelector 写在普通脚本里时,脚本位置和执行时机就很重要。
CSSOM 为什么会影响脚本
CSS 文件被解析成 CSSOM(CSS Object Model,CSS 对象模型)。浏览器通常需要知道样式,才能安全地绘制页面;某些情况下,脚本还可能读取样式或布局,因此脚本执行会等待已经发现的样式表。
<link rel="stylesheet" href="app.css">
<script>
console.log(getComputedStyle(document.body).color)
</script>不要把“CSS 阻塞渲染”简化成“CSS 阻塞所有 HTML 解析”。具体行为受脚本位置、预加载状态、媒体条件和浏览器调度影响。真正要判断时,看 Network 的请求时间和 Performance 的解析、样式计算区段。
async、defer 和 module
async 适合彼此独立、不依赖 DOM 顺序的脚本,例如某些统计代码。它下载完成就可能执行,因此不应依赖另一个 async 脚本先完成。
defer 适合需要完整 DOM、又不希望阻塞 HTML 解析的普通脚本。多个 defer 脚本通常按文档顺序执行,并在 HTML 解析完成后运行。
<script type="module" src="/main.js"></script>module script 默认具有 defer 式的解析关系,并且有模块依赖图、模块作用域和动态 import。它不是把普通 script 换个 type 就完全没有加载成本;依赖图中的模块还要被获取、解析和执行。
动态插入脚本和动态 import 是运行时行为:
const module = await import('./feature.js')
module.mount()它们适合把不需要首屏的功能推迟,但推迟也意味着用户第一次打开功能时仍要等待。优化不是把所有东西都 lazy load,而是区分首屏关键路径和交互后路径。
图片、字体和 iframe 何时被发现
图片通常在解析到 img 时被发现,字体可能要等 CSS 规则和实际文本使用到它才开始请求,iframe 还会启动另一个文档的导航。资源发现时间会影响下载优先级和首屏结果。
<img src="hero.webp" width="1200" height="700" alt="首页主图">
<link rel="preload" as="font" href="/inter.woff2" crossorigin>提前写图片尺寸可以减少布局跳动;preload 可以提前告诉浏览器某资源重要,但声明错误会浪费连接和带宽。prefetch 通常表达“未来可能需要”,优先级不应和当前首屏资源混在一起。
DOMContentLoaded 和 load
DOMContentLoaded 表示文档解析完成,并且相关可阻塞解析的脚本处理到允许事件触发的阶段;它不保证图片、背景图、字体和 iframe 全部完成。load 通常等待页面资源加载完成,但“完成”不等于用户体验已经良好。
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 可以查询了')
})
window.addEventListener('load', () => {
console.log('页面资源阶段结束')
})如果页面是从 bfcache 恢复,重点事件可能是 pageshow,而不是重新经历完整的 DOMContentLoaded。不要把一个事件当作所有导航形态的通用“页面终于好了”。
容易踩的坑
DOMContentLoaded 主要描述 DOM 与阻塞脚本阶段,不等于页面完全显示。defer 也不是“慢一点的 async”:它保留文档顺序,async 不保证顺序。preload 加多了会抢关键带宽;DOM 建完后图片、字体、iframe 和动态 import 仍可能继续加载。