K 的一隅

浏览器 浏览器导航与页面加载

HTML 到底是怎么变成页面的:解析、脚本与资源加载

从拆开一箱施工材料说起,理解 HTML 解析、DOM/CSSOM、async、defer、module、资源发现与页面事件。

4 分钟阅读 更新于 2026-07-24
本文目录

拆一箱施工材料时,工人一边读图纸,一边发现水管、门窗和电线。有些材料一到就能并行准备,有些工作必须等前一步完成。浏览器解析 HTML 也不是先把全文读完、再统一执行;它边读边构建 DOM,边发现资源,遇到脚本和样式时会改变节奏。

先猜:三个 script 谁先运行

html
<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> 等标签时,解析器或预加载扫描器可能发现子资源,浏览器就可以开始调度下载。

js
console.log(document.readyState)
const title = document.querySelector('title')

解析仍在进行时,DOM 可能只有部分节点;脚本如果在文档前部运行,后面的元素还不存在。把 document.querySelector 写在普通脚本里时,脚本位置和执行时机就很重要。

CSSOM 为什么会影响脚本

CSS 文件被解析成 CSSOM(CSS Object Model,CSS 对象模型)。浏览器通常需要知道样式,才能安全地绘制页面;某些情况下,脚本还可能读取样式或布局,因此脚本执行会等待已经发现的样式表。

html
<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 解析完成后运行。

html
<script type="module" src="/main.js"></script>

module script 默认具有 defer 式的解析关系,并且有模块依赖图、模块作用域和动态 import。它不是把普通 script 换个 type 就完全没有加载成本;依赖图中的模块还要被获取、解析和执行。

动态插入脚本和动态 import 是运行时行为:

js
const module = await import('./feature.js')
module.mount()

它们适合把不需要首屏的功能推迟,但推迟也意味着用户第一次打开功能时仍要等待。优化不是把所有东西都 lazy load,而是区分首屏关键路径和交互后路径。

图片、字体和 iframe 何时被发现

图片通常在解析到 img 时被发现,字体可能要等 CSS 规则和实际文本使用到它才开始请求,iframe 还会启动另一个文档的导航。资源发现时间会影响下载优先级和首屏结果。

html
<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 通常等待页面资源加载完成,但“完成”不等于用户体验已经良好。

js
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM 可以查询了')
})

window.addEventListener('load', () => {
  console.log('页面资源阶段结束')
})

如果页面是从 bfcache 恢复,重点事件可能是 pageshow,而不是重新经历完整的 DOMContentLoaded。不要把一个事件当作所有导航形态的通用“页面终于好了”。

容易踩的坑

DOMContentLoaded 主要描述 DOM 与阻塞脚本阶段,不等于页面完全显示。defer 也不是“慢一点的 async”:它保留文档顺序,async 不保证顺序。preload 加多了会抢关键带宽;DOM 建完后图片、字体、iframe 和动态 import 仍可能继续加载。