终于来到浏览器加载html并渲染阶段了

第一次渲染时都发生了什么
生成DOM树

一、HTML 解析与 DOM 树生成流程
1. 流式解析(Incremental Parsing)
-
触发时机 :
浏览器无需等待完整 HTML 下载 ,而是在接收到首批数据包(如 8KB)后立即开始解析。 -
优势 :
- 减少白屏时间,提升首屏渲染速度。
- 并行处理网络传输与解析任务
2. 解析过程类比编程语言
步骤 | 类比编程语言解析 | 浏览器中的实现 |
---|---|---|
词法分析 | 将代码拆分为 token | 将 HTML 标签/属性拆分为节点 |
语法分析 | 构建语法树(AST) | 构建 DOM 树(节点层级关系) |
流式处理 | 边读源码边解析 | 边下载 HTML 边构建 DOM |
✅ 关键点 :
HTML 解析器是状态机驱动 的,遇到
<tag>
进入标签状态,遇到属性则记录键值对。
总结
- 浏览器通过流式解析 将 HTML 数据流实时转换为 DOM 树,
- 此过程高效且非性能瓶颈,但后续的 渲染树合成、布局、绘制 阶段(依赖 DOM 树)直接决定页面性能。
- 理解该机制是优化 FCP(首次内容绘制) 和 TTI(可交互时间) 的基础。
资源加载
一、资源加载与 HTML 解析的协同流程
1. 并行化处理模型
- 关键点 :
HTML 解析(主线程)与资源下载(网络线程)并行进行,互不阻塞。
二、预加载(Preload)的深度机制
1. 工作原理
html
<!-- 强制提前加载关键资源 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
- 效果 :
- 资源请求早于解析器发现实际标签 (如 CSS 中的
@font-face
)。 - 优先级提升至 Highest (Chrome 中为
High
)。
- 资源请求早于解析器发现实际标签 (如 CSS 中的
2. 使用场景
场景 | 预加载目标 | 收益 |
---|---|---|
首屏关键字体 | as="font" |
避免文字渲染延迟(FOIT) |
首屏大图 | as="image" |
提升 LCP(最大内容绘制)速度 |
核心异步 JS | as="script" |
提前加载但延迟执行 |
动态路由组件(SPA) | as="fetch" |
加速路由切换 |
⚠️ 陷阱 :
滥用预加载会挤占带宽,反而拖慢关键资源(如主文档 CSS/JS)。
三、资源加载的性能优化策略
1. 预加载最佳实践
html
<!-- 顺序优化:优先加载字体和首屏图片 -->
<link rel="preload" href="hero-image.webp" as="image" imagesrcset="...">
<link rel="preload" href="main-font.woff2" as="font" crossorigin>
<!-- 动态检测用户带宽,选择性预加载 -->
<script>
if (navigator.connection.effectiveType === '4g') {
const link = document.createElement('link');
link.rel = 'preload';
link.href = 'large-video.mp4';
link.as = 'video';
document.head.appendChild(link);
}
script>
阻塞解析的JavaScript
解析HTML的过程并不是一帆风顺的,如果浏览器在这个时候遇到内联的JavaScript或没有defer/async的script标签,就不得不阻塞HTML的解析,而是先下载并且执行JavaScript的内容。
一、JavaScript 阻塞解析的核心机制
1. 阻塞触发条件
当解析器遇到以下脚本时,会立即暂停 HTML 解析:
html
<!--! 内联脚本直接阻塞 -->
<script>
document.getElementById('root').innerHTML = '...';
script>
<!--! 外部脚本(无 async/defer)需下载+执行后恢复 -->
<script src="app.js"></script>
2. 阻塞原理
- 单线程限制 :HTML 解析与 JavaScript 执行共享主线程,互斥进行。
- DOM 一致性 :JS 可能通过
document.write()
或 DOM 操作改变文档结构,解析器必须等待。
二、阻塞行为的性能影响
场景 | 性能损耗 | 用户感知 |
---|---|---|
头部同步脚本 | 延迟首屏渲染 500ms~2s+ | 长时间白屏 |
大型脚本未拆分 | 主线程被占用 100ms~1s+ | 页面卡顿/无响应 |
链式阻塞(多脚本) | 阻塞时间叠加 → 指数级延迟 | 交互延迟 |
📊 真实案例 :
电商网站将同步脚本改为
defer
后,LCP 时间从 4.2s 降至 2.1s(来源:WebPageTest)
三、解决方案:异步加载策略
1. async
与 defer
的差异
html
<!--! 异步下载,下载完立即执行(不保证顺序) -->
<script async src="analytics.js"></script>
<!--! 异步下载,HTML 解析完成后按序执行 -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>
特性 | async |
defer |
---|---|---|
执行时机 | 下载完立即执行 | 等 HTML 解析完后执行 |
执行顺序 | 不保证顺序(谁先下载谁执行) | 严格按文档顺序执行 |
适用场景 | 独立脚本(如统计代码) | 依赖 DOM 或有执行顺序要求的脚本 |
2. 现代模块化方案
html
<!-- ES Modules 默认 defer 行为 -->
<script type="module" src="app.js"></script>
<!-- 动态导入实现按需加载 -->
<script>
import('cart.js').then(module => {
module.initCart();
});
script>
生成CSSOM
仅仅有了DOM,渲染并不会开始,因为如果给用户看一个没有CSS的页面其实意义并不大。所以,接下来浏览器需要解析样式并且生成对应的CSSOM,这个过程同样由主线程完成,浏览器在解析CSS后会根据选择器和规则生成对应的CSSOM
一、CSSOM 构建机制详解
1. CSS 解析与 CSSOM 树生成
- 关键特性 :
- 树形结构 :CSSOM 采用树状数据结构,实现样式继承(如
body
的font-size
自动继承给子元素) - 阻塞渲染:未加载完成的 CSS 会阻塞渲染树合成(Render Tree Construction)
- 并行解析 :CSS 解析与 HTML 解析并行进行(但渲染需等待两者完成)
- 树形结构 :CSSOM 采用树状数据结构,实现样式继承(如
2. 浏览器默认样式(User Agent Stylesheet)
元素 | 默认样式示例 | 作用原理 |
---|---|---|
<h1> |
font-size: 2em; margin: 0.67em 0; |
提供基础视觉层次 |
<p> |
margin: 1em 0; |
确保段落间有默认间距 |
<ul> |
list-style-type: disc; |
定义列表项标识符 |
生成渲染树
浏览器从DOM树开始遍历节点,并在CSSOM中找到每个节点对应的样式规则,将两者合并成一棵树用于渲染,即渲染树,如图所示。

- 关键规则 :
- 可见性筛选 :不包含
display: none
的元素(但包含visibility: hidden
) - 样式继承:父节点样式自动传递给子节点(可被显式覆盖)
- 层叠计算 :按 CSS 优先级(特异性、顺序、
!important
)解析冲突样式
- 可见性筛选 :不包含
渲染树 vs DOM 树
特性 | DOM 树 | 渲染树(Render Tree) |
---|---|---|
节点类型 | 包含所有 HTML 元素 | 仅可见元素 |
结构关系 | 父子/兄弟关系完整 | 仅保留可见节点的层级关系 |
数据内容 | 包含文本/注释等非渲染节点 | 只含影响视觉呈现的节点 |
更新代价 | 修改非可见节点代价低 | 修改任何节点触发重排/重绘 |
计算布局
一、核心概念解析
1. 渲染树(Render Tree)
- 是什么 :由 DOM 树 + CSSOM 树合并而成,仅包含可见元素 (如
display: none
的元素不包含在内) - 作用 :确定元素的内容 和计算样式 (如颜色、字体大小),但不包含位置和尺寸
2. 布局(Layout)
- 核心任务 :计算渲染树中每个元素的精确几何信息 :
- 位置坐标(x, y)
- 尺寸(width, height)
- 边框/内边距(border, padding)
- 相对于视口的位置
- 输入:渲染树(样式+内容)
- 输出 :布局树(Layout Tree)(包含几何信息的渲染树)
3. 为什么需要布局?
- 元素相互影响 :网页是流式布局,任何元素尺寸/位置变化都会触发连锁反应
- CSSOM 的局限 :CSSOM 只存储样式值(如
margin: 10px
),但无法预知元素最终占用的实际空间
二、布局过程详解
1. 布局计算流程
步骤 | 描述 | 示例场景 |
---|---|---|
1. 根布局 | 计算 <html> 尺寸(通常基于视口宽度) |
移动端根字体大小计算 |
2. 流式布局 | 处理正常文档流元素(块级垂直堆叠,内联水平排列) | <div> 默认布局 |
3. 浮动处理 | 计算浮动元素位置,处理文本环绕 | float: left 的图片 |
4. 定位计算 | 处理绝对定位/固定定位元素 | position: fixed 的导航栏 |
5. 弹性/网格 | 计算 Flexbox/Grid 布局的复杂空间分配 | display: flex 的容器 |
2. 布局算法类型
布局模型 | 特点 | 性能影响 |
---|---|---|
正常流 | 默认布局方式,自上而下计算 | 高效但嵌套深时成本高 |
Flexbox | 单向弹性布局,动态分配空间 | 优于传统浮动 |
Grid | 二维网格布局,精准控制行列 | 复杂但计算效率高 |
绝对定位 | 脱离文档流,相对于父容器定位 | 局部重排,影响较小 |
三、布局树(Layout Tree)的生成
1. 与渲染树的区别
特性 | 渲染树(Render Tree) | 布局树(Layout Tree) |
---|---|---|
内容 | 元素+计算样式 | 元素+几何信息(位置/尺寸) |
节点类型 | 仅可见元素 | 增加匿名盒子(如文本换行产生的行盒) |
数据结构 | 树状结构 | 包含空间坐标的树 |
2. 布局树生成过程
分层
分层(Layer Tree)的作用与原理
问题背景
当浏览器完成布局树(Layout Tree)构建后,需解决元素绘制顺序问题:
- 元素可能因
z-index
、position
、3D变换(transform
)等属性相互覆盖 - 若无明确层级管理,会导致渲染错误(如本应置顶的元素被遮挡)
解决方案:生成图层树(Layer Tree)
-
图层划分条件 :
- 显式声明:
will-change: transform/opacity
、position: fixed
、z-index
- 隐式触发:3D变换(
transform: translate3d
)、video>
/canvas>
元素
- 显式声明:
-
图层树结构 :
plaintextLayer Tree (根图层) ├── 背景层 (z-index: 0) ├── 内容层 (z-index: 1) └── 弹窗层 (z-index: 9999) // 独立图层,确保置顶
-
优势 :
- 明确绘制顺序,避免元素覆盖错误
- 增量更新:修改单个图层时无需重绘整个页面(如滚动时只更新固定定位的导航栏图层)
绘制
光栅化(Rasterize)与合成(Compositing)
核心流程
关键步骤详解:
- 光栅化(Rasterize)
- 任务 :将图层的矢量描述 (如CSS盒子、文字)转换为像素点阵(位图)
- 执行者:光栅化线程(多线程并行)
- 优化:分块(Tiling)
- 将大图层拆分为 256x256 或 512x512 的小块(Tile)
- 优势 :
- 优先光栅化可视区域(Viewport)内的块
- 滚动时动态加载邻近块
2. 合成(Compositing)
- 任务:将光栅化后的块层进行合并,组合成最终图像
- 执行者 :合成线程 + GPU
- 输出:合成帧(Compositor Frame)直接提交给GPU显示
线程分工与性能优化
线程架构
线程 | 职责 | 是否阻塞主线程 |
---|---|---|
主线程 | DOM解析、样式计算、布局、图层生成 | 是(可能卡顿) |
合成线程 | 图层分块、调度光栅化、生成合成帧 | 否 |
光栅化线程 | 将图层块转为位图 | 否 |
纯合成动画的优势
plaintext
修改 transform 或 opacity → 合成线程直接处理 → GPU更新画面
- 无需经过主线程:跳过 JavaScript、布局(Layout)、绘制(Paint)等耗时步骤
- 60fps流畅动画:合成线程独立运作,不受JS执行卡顿影响
- 典型场景 :页面滚动、CSS3动画(
transform
,opacity
)
⚠️ 非纯合成动画(如修改
width
/height
)需触发重排/重绘,性能较差。
渲染流程总结
浏览器完整渲染流程
Layout Tree] F --> G[图层树
Layer Tree] G --> H[合成线程] H --> I[分块
Tiling] I --> J[光栅化线程池
Rasterization] J --> K[GPU内存] K --> L[合成帧
Compositor Frame] L --> M[GPU显示]
阶段详解:
-
主线程工作
- HTML解析:流式解析字节 → Token → DOM节点 → DOM树
- CSS解析:解析样式规则 → CSSOM树(含层叠优先级)
- 渲染树合并:DOM + CSSOM → 渲染树(仅含可见元素)
- 布局计算:计算元素几何属性 → 布局树(精确位置/尺寸)
- 图层分层:根据堆叠上下文生成图层树(Layer Tree)
-
合成线程接管
- 图层分块:将图层拆分为 256x256/512x512 的瓦片(Tile)
- 优先级调度:可视区域(Viewport)内的块优先处理
-
光栅化线程池
- 并行光栅化:多线程将矢量图块转为位图(像素数据)
- GPU存储:结果存入GPU显存(纹理Texture)
-
最终合成
- 合成帧生成:组合所有瓦片为完整帧
- GPU显示:通过图形驱动输出到屏幕(16.7ms/帧)

如何优化html首屏渲染?
一、尽快返回 HTML:流式渲染的核心价值
为什么重要?
- 瀑布流依赖:浏览器必须解析 HTML 才能发现 CSS/JS/图片等资源
- 首字节时间(TTFB):HTML 到达越早,渲染流程启动越早
- 数据对比:流式 HTML 可使首屏时间缩短 30-50%(Google 案例)
实现方案:
-
服务端流式渲染(SSR Streaming)
js// Node.js 示例(React) import { renderToNodeStream } from 'react-dom/server' app.get('/', (req, res) => { res.write('<!DOCTYPE html><head>...初始结构...') const stream = renderToNodeStream(<App />) stream.pipe(res, { end: false }) stream.on('end', () => res.end('body></html>')) })
- 效果:浏览器在服务端生成 HTML 时即可开始解析
-
HTTP/2 Server Push
nginx# Nginx 配置 http2_push /style.css; http2_push /app.js;
- 在返回 HTML 时主动推送关键资源
-
分块传输编码(Transfer-Encoding: chunked)
- 动态内容边生成边发送,无需等待完整 HTML
二、减少资源阻塞:关键路径优化
阻塞原理与解决方案:
资源类型 | 阻塞行为 | 优化方案 |
---|---|---|
CSS | 阻塞渲染树构建 | 1. 内联关键CSS 2. 异步加载非关键CSS 3. 使用media="print" 延迟非首屏CSS |
JS | 阻塞DOM解析和执行 | 1. defer /async 属性 2. 动态注入脚本 3. 模块按需加载 |
字体 | 阻塞文本渲染(FOIT) | 1. font-display: swap br>2. 预加载link rel="preload"> |
图片 | 不阻塞解析但占用带宽 | 1. loading="lazy" br>2. 响应式图片srcset |
关键CSS内联示例:
html
<head>
<style>
/* 内联首屏关键CSS(14KB) */
body, h1, .header { ... }
</style>
<!-- 异步加载剩余CSS -->
<link rel="preload" href="full.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="full.css"></noscript>
head>
JS加载最佳实践:
html
<!-- 高优先级关键脚本 -->
<script src="runtime.js" defer></script>
<!-- 低优先级脚本 -->
<script src="analytics.js" async></script>
<!-- 交互后加载的脚本 -->
button onclick="import('./modal.js')">打开弹窗button>
为什么DOM操作很慢
在16ms内完成单帧渲染以维持60fps流畅度
一、浏览器帧渲染全流程解析 -- 渲染一帧要干多少事!
要保持页面流畅,就是要保证这些帧能够在有限的时间内被渲染出来。
各种DOM操作对于浏览器来说意味着什么呢?上面介绍的渲染过程只是其中的一帧,而每一帧的处理耗时在很大程度上取决于这一帧是否要完全重新执行一遍上面所有的操作
重排
一、重排(Reflow)的本质
触发条件与执行流程
关键定义 :当元素的几何属性(位置、尺寸)发生变化时,浏览器必须重新计算所有元素的位置关系,这个过程称为重排(Reflow),会触发完整的渲染流水线(包括后续的重绘)。
二、导致重排的高频操作
1. 几何属性修改
js
element.style.width = '100px'; // 触发重排
element.style.height = '200px'; // 触发重排
element.style.margin = '10px'; // 触发重排
2. 内容变化
js
element.innerHTML = '<div>新内容div>'; // 新增DOM节点触发重排
element.textContent = '新文本'; // 文本高度变化触发重排
3. 视窗交互
js
window.addEventListener('resize', callback); // 窗口大小变化
window.addEventListener('scroll', callback); // 滚动(特定操作)
4. 布局信息读取(强制同步重排)
js
// 读取布局信息会强制刷新渲染队列
const width = element.offsetWidth; // 触发同步重排!
const height = element.clientHeight;
三、重排的性能灾难案例
案例1:图片加载导致布局抖动
html
<!-- 未设置尺寸的图片 -->
<img src="large-image.jpg"> <!-- 加载前高度为0 -->
<!-- 图片加载完成后 -->
<!-- 高度突变 → 下方内容被挤下去 → 触发重排 -->
后果:
- 页面内容突然跳动(Bad UX)
- CLS(Cumulative Layout Shift)指标恶化 CLS:衡量视觉稳定性的核心指标,值越高体验越差
解决方案:
html
<!-- 固定宽高比容器 -->
<div class="img-container" style="aspect-ratio: 16/9">
<img src="large-image.jpg" alt="">
div>
<!-- 或预留空间 -->
<img src="placeholder.jpg"
width="800"
height="600"
data-src="real-image.jpg">
案例2:循环中读写布局属性
js
// 灾难代码:强制同步布局(Layout Thrashing)
for (let i = 0; i < 100; i++) {
element.style.left = i + 'px'; // 写操作
console.log(element.offsetTop); // 读操作 → 强制重排!
}
后果:
- 100次重排 → 帧耗时可能超过500ms(严重卡顿)
四、主线程瓶颈的科学解析
浏览器架构中的主线程
- 单线程限制:JS执行、样式计算、布局、绘制都在同一条线程
- 阻塞效应:长时间JS任务 → 阻塞渲染 → 掉帧卡顿
重绘
一、重绘(Repaint)与重排(Reflow)的关系
渲染流水线对比:
关键区别:
特性 | 重排 | 重绘 |
---|---|---|
触发条件 | 几何属性改变 | 非几何视觉属性改变 |
性能消耗 | 高(全链路更新) | 中(跳过布局计算) |
常见操作 | width/height/margin 等 |
color/background/border 等 |
优化优先级 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
案例:修改按钮颜色仅触发重绘,修改按钮尺寸会触发重排+重绘
访问DOM属性
一、DOM访问的隐藏成本
1. DOM对象的位置
-
存在位置 :DOM树存储在主线程的内存空间中
-
管理方式:由渲染引擎(Blink/WebKit)直接管理
-
访问限制:只能在主线程中直接访问和修改
2. JavaScript引擎的位置
-
运行环境 :JavaScript引擎(V8等)也运行在主线程
-
特殊隔离 :但引擎内部通过内存隔离实现线程安全
-
访问机制:JS不能直接操作DOM内存空间,必须通过线程间通信
DOM与JS引擎架构解析:
线程间通信机制
javascript
// 伪代码展示线程间通信
// JavaScript线程发出请求
const request = {
command: 'GET_ELEMENT',
args: ['#myElement']
};
// 通过IPC发送到DOM线程
ipcRenderer.send(request);
// DOM线程处理请求
ipcMain.on(request, () => {
const element = document.querySelector(request.args[0]);
const response = {
id: element.id,
rect: element.getBoundingClientRect()
};
ipcRenderer.send(response);
});
强制重排
由于重排的性能损耗很大,一个元素的变动往往会触发大量相关元素的布局重新计算,因此浏览器一般会等待一段时间再进行批量处理。
然而,当从JavaScript中获取一些和排版有关的信息时,为了保证信息的正确性,浏览器不得不放弃这项优化,同步计算样式和排版信息并且返回给JavaScript,这个过程称为强制重排(Force Reflow)。
触发强制重排的API全解析
1. 几何属性访问
属性/方法 | 触发阶段 | 替代方案 |
---|---|---|
element.offsetTop |
同步Layout | 缓存值 |
element.offsetWidth |
同步Layout | transform 动画 |
element.getBoundingClientRect() |
同步Layout | 慎用 |
2. 滚动操作
js
window.scrollTo(0, 100); // 触发同步重排
element.scrollTop = 200; // 触发同步重排
3. 鼠标位置获取
js
element.addEventListener('mousemove', (e) => {
console.log(e.offsetX, e.offsetY); // 每次触发都重排!
});
4. 样式计算
js
// 获取最终计算样式 → 触发重排
const styles = getComputedStyle(element);
const width = styles.width;
如何优化DOM操作
批量操作
一、浏览器批量处理机制原理
渲染队列(Render Queue)工作流程:
但暂不执行 JS线程->>渲染引擎: 读取offsetHeight (查询布局) 渲染引擎-->>JS线程: 立即执行队列中所有操作! 渲染引擎-->>JS线程: 返回最新高度值
关键特性:
- 延迟执行:浏览器会将DOM修改操作缓存到队列
- 批量处理:同一执行上下文中的多次修改会被合并
- 读操作打破优化:任何布局查询都会强制立即执行队列
二、手动批量操作技巧
1. 文档片段(DocumentFragment)
javascript
// 创建虚拟容器
const fragment = document.createDocumentFragment();
// 批量创建元素(不在DOM树中操作)
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
// 单次插入DOM(仅1次重排)
document.getElementById('container').appendChild(fragment);
2. 离线DOM操作
javascript
// 1. 克隆节点到内存
const original = document.getElementById('list');
const clone = original.cloneNode(true);
// 2. 在副本上批量修改
Array.from(clone.children).forEach(child => {
child.classList.add('updated');
});
// 3. 单次替换原节点(仅1次重排)
original.parentNode.replaceChild(clone, original);
三、React批量更新机制解密
batchUpdate工作原理:
代码示例:
jsx
function Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
// React自动批处理
setCount(c => c + 1); // 更新1
setCount(c => c + 1); // 更新2
// 最终只触发1次重排
};
return <button onClick={handleClick}>Count: {count}</button>;
}
纯合成动画
一、合成渲染核心原理
浏览器渲染管线对比:
合成层创建条件:
- 独立坐标空间 :
- 3D变换:
transform: translate3d()
- 透视效果:
perspective()
- 3D变换:
- 特殊元素 :
<video>
,<canvas>
,<iframe>
position: fixed
元素
- 显式提示 :
will-change: transform/opacity
transform: translateZ(0)
(旧浏览器hack)
二、纯合成动画三要素
1. 避免重排
属性类型 | 触发阶段 | 合成友好替代方案 |
---|---|---|
top/left |
重排+重绘 | transform: translate() |
width/height |
重排+重绘 | transform: scale() |
margin/padding |
重排+重绘 | 避免动画 |
2. 避免重绘
css
/* 触发重绘 ❌ */
.box {
animation: color-change 1s infinite;
}
@keyframes color-change {
0% { background: red; }
100% { background: blue; }
}
/* 纯合成 ✅ */
.box {
animation: move 1s infinite;
}
@keyframes move {
0% { transform: translateX(0); }
100% { transform: translateX(100px); }
}
3. 元素提升至合成层
css
.optimized {
will-change: transform; /* 现代浏览器 */
transform: translateZ(0); /* 兼容旧版 */
backface-visibility: hidden; /* 辅助提升 */
}
总结:
又是一篇超长文,读完我相信对 react,vue的 设计初心应该有更深的理解