公众号:小博的前端笔记
一:渲染机制
核心目标: 将 HTML、CSS、JavaScript 代码转换为用户可见、可交互的像素点。
关键渲染路径: 浏览器完成页面首次渲染所必须经过的一系列步骤。优化关键渲染路径是提升页面加载性能的关键。
主要阶段:
-
解析(Parsing)与构建 DOM / CSSOM 树:
-
输入: 接收 HTML 和 CSS 字节流。
-
过程:
- 词法分析: 将字节流分解成有意义的标记(Token)。
- 语法分析: 根据 HTML/CSS 语法规则,将这些标记构建成树形结构。
-
输出:
- DOM(Document Object Model)树: HTML 的树形结构表示。每个 HTML 元素(标签、属性、文本)都是树中的一个节点(Node)。DOM 树捕获了文档的内容和结构。
- CSSOM(CSS Object Model)树: CSS 的树形结构表示。每个 CSS 规则及其包含的选择器和声明都被映射到树中。CSSOM 树捕获了应用于文档的样式信息。
-
关键点:
-
渐进式构建: 浏览器在接收到部分 HTML/CSS 后就会开始解析和构建,无需等待全部下载完成。
-
阻塞行为:
<link rel="stylesheet">
: 会阻塞渲染树的构建(Render Tree Construction)和首次渲染(Painting),但通常不会阻塞 DOM 的构建(现代浏览器)。 浏览器需要完整的 CSSOM 来计算样式。因此,它会阻塞后续的渲染步骤,直到 CSSOM 构建完成。
-
优化提示:
- 将 CSS 放在文档头部 (
<head>
),尽早开始 CSSOM 构建。 - 将 JavaScript 放在文档底部 (
</body>
前),或者使用defer
/async
属性来避免阻塞 DOM 构建。 - 避免使用
@import
在 CSS 中引入其他 CSS 文件,因为它会增加 CSSOM 构建的延迟。 - 使用媒体查询 (
media="print"
,media="(max-width: 600px)"
) 让非关键 CSS 不阻塞渲染。
- 将 CSS 放在文档头部 (
-
-
-
构建渲染树(Render Tree / Frame Tree):
-
输入: DOM 树 + CSSOM 树。
-
过程:
- 浏览器将 DOM 树和 CSSOM 树合并。
- 遍历可见的 DOM 节点(不包括
head
、meta
、script[type="text/javascript"]
、display: none
的元素及其后代等),并为每个需要视觉渲染的节点找到匹配的 CSSOM 规则。 - 应用这些样式规则,生成一棵只包含可见内容 及其计算样式的树------渲染树。
-
输出: 渲染树。渲染树节点通常称为"渲染对象"或"框"。
-
关键点:
- 渲染树只包含需要绘制到屏幕上的元素。
visibility: hidden
的元素会包含在渲染树中(因为它占据空间),而display: none
的元素则不会。- 每个渲染对象知道如何将自己及其子元素绘制出来。
-
-
布局(Layout) / 重排(Reflow):
-
输入: 渲染树。
-
过程:
- 计算渲染树中每个节点在视口(Viewport) 内的确切位置(x, y 坐标)和大小(宽度、高度)。这是一个递归过程。
- 确定所有元素的几何信息(位置、尺寸)。
-
输出: 每个渲染对象的精确坐标和尺寸信息。
-
关键点:
-
"流式布局模型": 网页通常采用流式布局,大多数元素的位置是根据其兄弟元素和父元素的位置计算出来的。
-
全局布局 vs 增量布局: 首次构建渲染树后的布局称为"全局布局"。之后对渲染树的修改(如改变元素尺寸、位置、添加/删除元素)可能只触发受影响部分的"增量布局"(Reflow)。
-
性能消耗大: 布局是计算密集型操作,频繁触发布局(重排)会严重影响性能。
-
触发重排的操作示例:
- 添加、删除、移动 DOM 元素。
- 改变元素尺寸(
width
,height
,padding
,border
,margin
)。 - 改变元素位置(
position
,top
,left
)。 - 改变窗口大小(
resize
事件)。 - 改变字体大小或内容(如文本输入框输入文字)。
- 激活 CSS 伪类(如
:hover
导致尺寸变化)。 - 读取某些布局属性(如
offsetWidth
,offsetHeight
,getComputedStyle()
)会强制浏览器执行同步布局(也称为"强制同步布局"或"布局抖动"),以获得最新值。
-
-
-
绘制(Painting) / 栅格化(Rasterization):
-
输入: 经过布局计算的渲染树。
-
过程:
-
绘制(Painting): 将每个渲染对象分解成多个绘制指令(如"画矩形"、"画文字"、"画边框"、"画背景")。这是一个记录绘制步骤的过程,发生在主线程。
-
栅格化(Rasterization): 将绘制指令实际执行 ,转换成屏幕上的像素点 。这个过程通常由合成线程(Compositor Thread) 将任务分发给栅格化线程(Raster Threads) 在 GPU 上完成(硬件加速)。
- 现代浏览器会将页面划分为多个图层(Layers)。
- 每个图层独立栅格化(通常是在单独的栅格化线程中)。
- 图层内容发生变化时(如动画),只需重新栅格化该图层,而不是整个页面。
-
-
输出: 内存中的位图(Bitmap),表示页面特定区域的像素数据。
-
关键点:
-
重绘(Repaint): 当元素的外观(颜色、背景色、边框颜色、阴影等)改变,但不影响其几何属性(位置、大小)时,浏览器会触发重绘。重绘不一定会触发重排(Layout),但重排一定会触发重绘(因为布局改变后外观通常也需要更新)。
-
性能消耗: 绘制和栅格化也是消耗性能的操作,但通常比重排轻量。复杂的 CSS 效果(如阴影、渐变)会增加绘制时间。
-
分层(Layering)与合成(Composition):
- 浏览器会根据 CSS 属性(如
transform
,opacity
,will-change
,position: fixed
,video
元素)将元素提升到独立的合成层(Compositing Layer) 。 - 每个图层被栅格化后存储在 GPU 内存中。
- 合成(Composition): 合成线程负责收集所有图层(称为"合成器帧"),计算它们在视口中的最终位置(考虑滚动、缩放、变换),并将它们合成为一个最终的屏幕图像帧。这个过程非常高效,因为它主要是在 GPU 上操作位图。
- 浏览器会根据 CSS 属性(如
-
-
-
显示(Display):
-
过程: 合成线程将最终合成的位图帧提交给 GPU,由 GPU 将其扫描输出到显示器上。
-
关键点:
- 显示器通常以固定频率(如 60Hz,即每秒 60 次)刷新。
- 浏览器会尽量将新的合成帧与显示器的刷新周期(VSync)同步,以实现流畅的动画和滚动效果。这就是
requestAnimationFrame
API 的意义所在。
-
流程图总结:
scss
字节流 (HTML/CSS)
↓
解析 (Parsing) → [JS 执行可能阻塞 DOM 构建]
↓
构建 DOM 树 构建 CSSOM 树 → [CSS 阻塞渲染树构建]
↘ ↙
构建渲染树 (Render Tree) → [只含可见节点+计算样式]
↓
布局 (Layout / Reflow) → [计算精确位置/尺寸]
↓
绘制 (Painting) → [生成绘制指令列表]
↓
栅格化 (Rasterization) → [GPU上执行绘制指令,生成图层位图] (通常在合成线程/栅格线程)
↓
合成 (Composition) → [合成图层,形成最终帧]
↓
显示 (Display) → [GPU输出到屏幕]
延伸点/优化点:
-
重排(Reflow)与重绘(Repaint)的优化:
-
避免触发重排:
- 避免频繁操作 DOM(使用
DocumentFragment
或离线 DOM 进行批量修改)。 - 避免逐项修改样式,使用
class
或cssText
一次性修改。 - 避免在循环中读取会触发重排的布局属性(
offsetTop
,offsetLeft
,offsetWidth
,offsetHeight
,scrollTop
,scrollLeft
,scrollWidth
,scrollHeight
,clientTop
,clientLeft
,clientWidth
,clientHeight
,getComputedStyle()
)。如果必须读取,先将它们缓存起来。 - 对复杂动画元素使用绝对定位 (
position: absolute
或fixed
),使其脱离文档流,影响范围缩小。
- 避免频繁操作 DOM(使用
-
利用合成(Composition)优化动画: 优先使用
transform
(位移、缩放、旋转) 和opacity
属性来制作动画。这些属性可以由合成器线程直接在 GPU 上处理,跳过主线程的布局和绘制阶段,性能极高。 -
will-change
属性: 提示浏览器哪些元素可能会发生变化(如transform
,opacity
),让浏览器提前为其创建独立的合成层,优化后续变化的性能(但要谨慎使用,滥用会增加内存消耗)。 -
content-visibility: auto
: 现代 CSS 属性,可以跳过屏幕外内容的渲染(布局和绘制),大幅提升长页面加载和滚动性能。
-
-
脚本加载优化:
defer
: 脚本异步下载,在 DOM 解析完成之后、DOMContentLoaded
事件之前按顺序执行。不阻塞 DOM 构建。async
: 脚本异步下载,下载完成后立即执行(可能在 DOM 解析完成之前或之后)。执行时会阻塞 HTML 解析。适用于无依赖、不操作 DOM 的独立脚本。- 模块脚本 (
<script type="module">
): 默认具有defer
行为。
-
现代渲染架构(Chromium 为例):
- 主线程(Main Thread / Renderer Thread): 处理 HTML 解析、DOM 构建、CSS 解析、CSSOM 构建、JS 执行(大部分)、布局(Layout)、绘制(Painting - 生成指令列表)。
- 合成线程(Compositor Thread): 负责图层管理、滚动处理、动画处理(处理
transform
/opacity
的合成层动画)、将图层分块(tiles)分发给栅格线程、合成最终帧。 - 栅格线程(Raster Threads): 在 GPU 上执行绘制指令,将图层分块栅格化为位图。
- GPU 进程(GPU Process): 管理 GPU 资源,最终将合成线程提交的帧绘制到屏幕上。
回答技巧:
- 结构化清晰: 按阶段(解析、构建树、布局、绘制、合成、显示)一步步讲清楚。
- 突出关键点: 强调阻塞行为(JS 阻塞 DOM 构建,CSS 阻塞渲染树构建)、重排重绘的区别与优化、合成(Composition)的优势。
- 联系实际: 解释为什么要把 CSS 放头部、JS 放底部?为什么
transform
动画更高效?什么是强制同步布局? - 提及优化策略: 主动说出常见的优化手段(如避免重排、使用
transform
/opacity
、defer
/async
)。 - 了解核心概念: 确保理解 DOM、CSSOM、渲染树、图层(Layer)、合成(Composition)、重排(Reflow)、重绘(Repaint)等术语的含义。
- 结合浏览器架构(加分项): 如果深入,可以提一下主线程、合成线程、栅格线程的分工。
二:css树和dom树哪个先构建
核心结论:
DOM树和CSSOM树是并行构建的,但CSSOM的完成会阻塞渲染树构建,而同步JavaScript会阻塞DOM构建
详细构建流程:

关键规则解析:
-
并行启动:
-
浏览器同时开始解析HTML构建DOM树和下载/解析CSS构建CSSOM树
-
示例时间线:
makefile0ms: 开始解析HTML → 启动DOM构建 5ms: 发现<link> → 启动CSS下载 10ms: 发现<img> → 启动图片下载(非阻塞) 20ms: CSS下载完成 → 启动CSSOM构建
-
-
阻塞关系:
-
CSSOM构建:
- 不会阻塞DOM树的构建(现代浏览器)
- 但会阻塞渲染树构建(必须等待CSSOM完成)
- 会阻塞后续JavaScript执行(JS可能依赖样式)
-
JavaScript:
- 同步脚本(
<script>
)会立即阻塞DOM构建 - 遇到脚本时,必须等待当前所有CSS下载完成才执行(避免JS操作未解析的样式)
- 同步脚本(
-
-
特殊场景:
xml<head> <link href="style.css" rel="stylesheet"> <!-- 并行构建CSSOM --> <script> // 此脚本需等待style.css下载完成才执行! console.log(getComputedStyle(document.body).color) </script> </head> <body> <!-- DOM构建在此处被脚本阻塞 --> <div>内容</div> </body>
构建优先级总结:
资源类型 | 阻塞DOM构建 | 阻塞渲染树构建 | 并行性 |
---|---|---|---|
HTML解析 | - | - | 基础解析流 |
外部CSS | ❌ | ✅ | ✅ 并行下载 |
同步JavaScript | ✅ | ✅ | ❌ 顺序执行 |
异步JavaScript | ❌ | ❌ | ✅ 并行 |
图片 | ❌ | ❌ | ✅ 并行下载 |
最佳答案:
"浏览器会并行启动DOM树和CSSOM树的构建过程,但两者存在关键依赖:
- CSS不会阻塞DOM构建:现代浏览器能边下载CSS边构建DOM
- CSSOM会阻塞渲染:必须等CSSOM完成才能构建渲染树
- JavaScript是最大阻塞源:同步脚本会阻塞DOM构建,且需等待前置CSS下载完成
优化核心:CSS放头部快速构建CSSOM,JavaScript放底部或用async/defer避免阻塞DOM构建"
附加考点:
-
为什么CSS要放头部? 尽早开始CSS下载,避免渲染树构建延迟导致白屏时间过长
-
为什么JS放底部? 防止阻塞DOM构建,让用户更快看到页面骨架
-
async
vsdefer
的区别:async
:下载完立即执行,可能中断HTML解析defer
:下载完等待HTML解析完成后执行
三、DOM树结构和CSS树结构
DOM树和CSSOM树在底层不是普通的JavaScript对象 ,而是浏览器渲染引擎用C++/Rust等系统语言实现的高度优化的专用数据结构。下面从底层实现角度详细解析:
DOM树的对象结构
核心实现原理:
kotlin
// 以Chromium的Blink引擎为例(简化版C++)
class Node {
NodeType type; // 节点类型(元素/文本等)
Node* parent; // 父节点指针
Node* first_child; // 首子节点指针
Node* next_sibling; // 兄弟节点指针
AtomicString node_name; // 节点名称(如"div")
};
class Element : public Node {
HashMap<AtomicString, AtomicString> attrs; // 属性键值对
StylePropertyMap style_properties; // 内联样式
ComputedStyle* computed_style = nullptr; // 计算后的样式
};
class Text : public Node {
String data; // 文本内容
};
在JavaScript中的表现:
javascript
// 浏览器暴露给JS的DOM对象只是底层对象的包装器
const div = document.createElement('div');
// 实际内存结构:
JavaScript Heap
↓
[JS Wrapper Object] ← 通过V8引擎绑定
│
└──→ [C++ Node对象] ← Blink引擎内存
│
├── type: ELEMENT_NODE
├── tag: "DIV"
├── parent: <body的C++对象指针>
└── style: <指向CSSOM的StylePropertyMap>
🔥 关键真相:
- 你操作的
div
只是系统对象的JS代理- 每次访问
div.style
都会触发跨引擎通信(JS引擎 ↔ 渲染引擎)- DOM操作昂贵的原因:跨越语言边界 + 触发渲染管线更新
CSSOM树的对象结构
底层实现(以Blink为例):
kotlin
// CSS规则存储结构
class CSSRule {
CSSStyleSheet* parent_sheet;
Vector<CSSSelector> selectors; // 选择器列表
StylePropertySet properties; // 样式键值对
};
// 样式表结构
class CSSStyleSheet {
Vector<CSSRule> rules;
bool disabled;
};
// 计算样式缓存(每个元素独立)
class ComputedStyle {
const CSSValue* getProperty(CSSPropertyID id) const;
// 存储最终计算值,如:
// width: 100px → 实际像素值
// color: red → RGBA数值
};
JS访问时的表现:
ini
// 浏览器暴露的有限接口
const stylesheet = document.styleSheets[0];
const rule = stylesheet.cssRules[0];
// 底层实际结构:
JavaScript访问层
↓
[CSSStyleSheet JS对象] ← 受限代理
│
└──→ [C++ CSSStyleSheet对象]
│
├── rules: [C++ CSSRule列表]
└── 无法直接访问底层样式计算缓存
与普通JS对象的本质差异
特性 | 普通JS对象 | DOM/CSSOM对象 |
---|---|---|
存储位置 | JS引擎堆内存 | 渲染引擎专用内存 |
操作成本 | 纳秒级 | 微秒~毫秒级(涉及跨进程通信) |
内存结构 | 无序属性表 | 树形结构+样式继承链 |
修改代价 | 仅影响JS内存 | 可能触发重排/重绘 |
属性访问 | 直接内存读取 | 可能触发引擎间IPC通信 |
垃圾回收 | V8引擎管理 | 跨引擎协同回收 |
性能陷阱示例
ini
// 看似简单的操作,实际发生的事:
for(let i=0; i<1000; i++) {
div.style.width = i + 'px';
}
// 底层代价:
1. JS → C++: 1000次跨引擎通信
2. 每次修改:
- 更新CSSOM计算样式
- 检查是否触发重排(这里是会触发的!)
- 重新计算布局
3. 可能触发1000次重排!
高级调试技巧
在Chrome DevTools中验证:
ini
# 1. 访问原生函数(非JS包装器)
> document.body.appendChild.toString()
< "function appendChild() { [native code] }"
# 2. 查看隐藏的底层引用
> const div = document.createElement('div');
> %DebugPrint(div); # 需启用--allow-natives-syntax flag
< 0x1e3e25c0aed9: [JSObject]
- map=0x1e3e25d82ed1 [FastProperties]
- prototype=0x1e3e25d0b111
- elements=0x1e3e25d82671 [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store=0x55aabbcc3340 # ← 指向C++内存的指针!
终极总结
"DOM树和CSSOM树不是JavaScript对象,而是:
- 浏览器内核实现的C++树结构(如Blink中的Node/ComputedStyle)
- JS对象只是底层对象的跨语言代理(通过V8绑定实现)
- 每次访问
element.style
都涉及 JS引擎 ↔ 渲染引擎的IPC通信这解释了:
- 为什么DOM操作比JS对象慢100倍+
- 为什么前端框架要用虚拟DOM批量更新
- 为什么直接操作CSSOM的API受限(性能和安全考虑)"
四:什么是异步js
异步 JavaScript 的核心特征
不阻塞 DOM 解析,允许浏览器继续构建 DOM 树
异步 JS 的 5 种实现方式
1. async
属性(经典异步)
xml
<script src="app.js" async></script>
-
行为:
- 立即并行下载 脚本,下载完成立即中断 HTML 解析执行脚本
- 执行顺序:不可控(先下载完先执行)
-
适用场景:独立脚本(如埋点统计、广告加载)
2. defer
属性(延迟异步)
xml
<script src="app.js" defer></script>
-
行为:
- 并行下载脚本,但延迟到 DOM 解析完成后执行 (
DOMContentLoaded
前) - 执行顺序:按文档位置顺序执行
- 并行下载脚本,但延迟到 DOM 解析完成后执行 (
-
适用场景:依赖 DOM 的脚本(如页面初始化逻辑)
3. 动态脚本注入(高级异步)
ini
const script = document.createElement('script');
script.src = 'app.js';
document.head.appendChild(script); // 此时开始异步加载
-
行为:
- 默认具有
async
行为(可通过script.async=false
改为按顺序执行) - 完全不阻塞解析
- 默认具有
4. ES6 模块 (type="module"
)
xml
<script type="module" src="app.mjs"></script>
-
行为:
- 默认具有
defer
行为(延迟到 DOM 解析后执行) - 添加
async
属性可转为立即执行模式
- 默认具有
5. Web Worker(线程级异步)
arduino
const worker = new Worker('task.js'); // 在独立线程运行
-
行为:
- 在后台线程执行,完全不阻塞主线程渲染
同步 vs 异步 关键区别
特性 | 同步 JS (<script> ) |
异步 JS (async /defer /动态加载) |
---|---|---|
阻塞 DOM 构建 | ✅ 立即停止解析 | ❌ 不阻塞 |
执行时机 | 下载完立即执行 | async :下载完立即执行 defer :DOM 解析后 |
执行顺序 | 按文档顺序执行 | async :乱序 defer :顺序 |
依赖 DOM | 可能操作未解析的 DOM | defer 可安全操作完整 DOM |
黄金答案
"异步 JavaScript 是指 不阻塞 HTML 解析器 的脚本加载方式,包含 5 种实现:
async
属性:并行下载,下载完立即执行(可能中断解析),执行顺序不可控defer
属性:并行下载,DOM 解析完成后按序执行- 动态脚本注入 :通过 JS 创建的
<script>
默认异步- ES Module (
type="module"
):默认等效defer
- Web Worker:在独立线程运行,彻底避免阻塞
核心价值:加速首屏渲染,避免白屏时间过长"
⚠️ 常见面试陷阱
问题:
xml
<head>
<script async src="A.js"></script>
<script defer src="B.js"></script>
<script src="C.js"></script>
</head>
执行顺序是什么? 答案 : C.js
→ A.js
(如果先下载完) → B.js
解析:
- 同步脚本
C.js
立即阻塞解析优先执行 async
脚本A.js
下载完可能抢在B.js
前执行defer
脚本B.js
最后执行(DOM 解析后)
性能优化必记法则
