浏览器完整渲染流程
以单页应用(SPA)为例,从输入URL到首屏展现,其核心流程可以清晰地划分为几个关键阶段。下面我们逐步拆解,看看浏览器在每个阶段都做了什么。
阶段 1:加载 HTML 和初步解析
- 输入 URL:浏览器请求
index.html(若为 SPA 的history模式,服务器需配置所有路径返回该文件)。 - 返回 HTML:服务器返回
index.html(可能包含内联 CSS/JS 或外链资源)。 - 创建渲染进程:浏览器分配渲染进程(同源页面可能复用)。
阶段 2:解析 HTML,构建 DOM 和 CSSOM
- 解析 HTML(Blink 引擎):
- 遇到
<link rel="stylesheet">:异步下载 CSS,但会阻塞渲染树的构建(必须等 CSSOM 完成)。 - 遇到
<script>: -
- 无属性:同步下载并执行,阻塞 DOM 解析。
defer:异步下载,在 DOM 解析完成后、DOMContentLoaded前按顺序执行(Vue 默认行为)。async:异步下载,下载完立即执行(顺序不确定)。
- 遇到
<img>:异步加载,不阻塞解析。
- 构建 DOM 树:逐步生成 DOM 节点(此时 Vue 的
#app只是一个空div)。 - 构建 CSSOM 树:同步解析内联
<style>或下载完成的 CSS 文件。
阶段 3:触发 DOMContentLoaded 事件
- 触发条件:
- DOM 树构建完成(所有同步 HTML 解析完毕)。
- 所有同步 JS(无
async/defer)和deferJS 执行完毕。 - CSSOM 构建完成(即使 CSS 是异步加载的,也必须等它解析完)。
- 此时状态:
- 浏览器已掌握完整的 DOM 和 CSSOM,可以构建渲染树。
- Vue 项目:
defer的app.js已执行,Vue 完成挂载,虚拟 DOM 转为真实 DOM。
阶段 4:渲染页面(Render Tree → 绘制)
- 合并 DOM + CSSOM:生成渲染树(Render Tree),排除
display: none的元素。 - 布局(Layout):计算每个节点的尺寸和位置(如
width: 50%转为实际像素)。 - 绘制(Paint):将布局结果转为屏幕像素(可能触发重排/重绘)。
- 合成(Composite):GPU 加速渲染图层(如
transform动画)。
✅ 用户此时看到首屏内容(即使图片或异步资源未加载完)。
阶段 5:触发 load 事件
- 触发条件:所有资源(图片、字体、异步脚本、iframe 等)加载完成。
- 关键区别:
DOMContentLoaded:DOM + CSSOM 就绪,页面已渲染。load:所有资源就绪,不重新渲染,仅标志“完全加载”。
阶段 6:Vue 路由切换(SPA 特性)
- 用户点击链接:Vue Router 拦截导航,不刷新页面。
- 异步加载组件:若配置了懒加载(如
() => import('./User.vue')),请求对应 JS 文件。 - 局部更新 DOM:Vue 通过虚拟 DOM diff 更新页面,无需重新渲染整个文档。
关键概念总结
| 事件/阶段 | 触发条件 | Vue 项目的表现 |
|---|---|---|
| HTML 解析 | 下载并解析 index.html | 遇到 #app 空容器 |
DOMContentLoaded | DOM + CSSOM 就绪,defer JS 执行完 | Vue 完成挂载,首屏显示 |
| 渲染树 → 绘制 | 合并 DOM + CSSOM,布局,绘制 | 用户看到内容(可能缺图片) |
load | 所有资源加载完成 | 图片/字体等加载完毕,不重新渲染 |
| 路由切换 | 用户交互触发导航 | 异步加载组件,局部更新 DOM |
常见误区澄清
- 误区:
load事件后会重新渲染页面。 真相:渲染早在DOMContentLoaded后完成,load只是资源加载完成的回调。 - 误区:Vue 首屏一定是空
div。 真相:SSR/预渲染/骨架屏可优化首屏内容。
最终流程图
这样梳理后,是否完全理清了浏览器的渲染机制? 😊