1. 浏览器从 HTML 到最终显示,完整过程是什么?
核心回答
浏览器先解析 HTML 和 CSS,生成 DOM 和 CSSOM,再计算布局、分层、生成绘制指令,经过分块和光栅化,最后把各个图层合成到屏幕上。
展开
可以把现代 Chromium 的渲染过程理解成:
text
HTML
↓
解析
↓
DOM Tree
CSS
↓
解析
↓
CSSOM
│
└──────┐
↓
样式计算 / 布局
↓
布局树
↓
分层
↓
绘制
↓
分块
↓
光栅化
↓
合成
↓
显示
注意,这不是简单的"一个步骤执行完才执行下一个步骤"。
HTML 解析、CSS 加载/解析、资源加载、JavaScript 执行之间存在并行和相互阻塞关系。
真正面试高级的地方,是继续回答:
什么时候能继续?什么会阻塞?什么变化会重新走哪一步?
2. HTML 解析的时候,浏览器到底做了什么?
核心回答
HTML Parser 会把 HTML 字节流逐步解析成 DOM,而不是等整个 HTML 下载完才一次性生成 DOM。
例如:
html
<html>
<body>
<div>
<span>Hello</span>
</div>
</body>
</html>
大致是:
text
HTML 字节流
↓
词法分析
↓
Token
↓
DOM Node
↓
DOM Tree
所以浏览器可以边下载、边解析、边构建 DOM。
3. HTML 解析和 CSS 解析是串行还是并行?
核心回答
不是简单的串行,它们可以并行推进;HTML 解析发现 CSS 后可以发起 CSS 请求,同时继续解析 HTML。
例如:
html
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div>Hello</div>
</body>
可以理解成:
text
HTML Parser
│
┌─────────┴─────────┐
↓ ↓
构建 DOM 发现 CSS
↓
请求 CSS
↓
CSS Parser
↓
CSSOM
所以不要说:
HTML 解析完以后才开始 CSS 解析。
这不准确。
4. CSS 会不会阻塞 HTML 解析?
核心回答
普通外部 CSS 一般不会直接让 HTML Parser 停下来等 CSS 下载完,但 CSS 会影响后续的样式计算和渲染,而且还可能间接影响脚本执行。
所以一定要区分:
text
CSS 阻塞 HTML 解析?
↓
通常不是直接阻塞
CSS 阻塞渲染?
↓
关键 CSS 会影响首次渲染
CSS 影响 JS 执行?
↓
某些情况下会
这也是面试里很容易被追问的地方。
5. 为什么 CSS 会影响首次渲染?
核心回答
因为浏览器必须知道元素最终是什么样子,才能正确计算布局和绘制,所以关键 CSS 没准备好时,通常不能完成正确的首次渲染。
例如:
css
.box {
display: none;
}
如果浏览器完全不管 CSS,直接把:
html
<div class="box">Hello</div>
画出来,之后 CSS 到了再隐藏,就会产生错误的中间画面。
所以:
text
DOM
│
│
CSSOM
│
└──────┐
↓
样式计算
↓
布局树
↓
绘制
6. HTML 遇到 <script> 会发生什么?
核心回答
普通同步 script 会暂停 HTML 解析,浏览器先下载并执行脚本,执行结束后 HTML Parser 才继续。
例如:
html
<div>1</div>
<script src="app.js"></script>
<div>2</div>
大致:
text
解析 <div>1</div>
↓
发现 script
↓
暂停 HTML Parser
↓
下载 JS
↓
执行 JS
↓
继续 HTML Parser
↓
解析 <div>2</div>
这也是为什么大量同步脚本放在 HTML 前面可能拖慢页面构建。
7. defer 和 async 到底解决什么问题?
核心回答
defer 是"异步下载、HTML 解析完再按顺序执行";async 是"异步下载、谁先下载完谁先执行"。
普通 script
text
HTML解析
↓
发现JS
↓
暂停解析
↓
下载 + 执行JS
↓
继续解析
defer
text
HTML解析 ─────────────────→ 完成
│
└── JS异步下载
↓
HTML解析完成
↓
执行JS
多个 defer:
text
a.js → b.js → c.js
保持文档顺序。
async
text
HTML解析 ─────────────────→
│
└── JS异步下载
↓
下载完成
↓
立即执行
所以多个 async:
text
a.js ──┐
b.js ──┼── 谁先下载完谁先执行
c.js ──┘
8. DOM、CSSOM 之后为什么不是直接"Render Tree"?
核心回答
现代浏览器更值得关注的是样式计算和布局树,而不是把 Render Tree 当成一个独立的核心阶段死记。
可以理解成:
text
DOM
+
CSSOM
↓
样式计算
↓
布局树
布局树描述的是:
最终参与布局的内容,以及它们的几何信息。
例如:
css
display: none;
对应的节点不会正常参与布局。所以布局树和 DOM Tree 并不是一模一样。
9. 什么是布局?
核心回答
布局就是确定每个元素最终放在哪里、尺寸是多少,以及它和其他元素之间的几何关系。
例如:
html
<div class="box"></div>
css
.box {
width: 200px;
height: 100px;
margin-left: 50px;
}
布局阶段需要确定类似:
text
x = 50
y = ...
width = 200
height = 100
所以:
text
样式
↓
布局
↓
确定几何信息
10. 什么是分层?为什么需要分层?
核心回答
分层就是把页面中的部分内容放到独立的图层里,后续这些内容可以独立参与绘制和合成。
可以简单理解:
text
页面
├── Layer A
├── Layer B
└── Layer C
例如某些具有:
css
transform
opacity
或者其他特定条件的元素,可能被提升到独立合成层。这样做的价值是:
text
普通情况:
修改元素
↓
重新布局/绘制
↓
重新处理
独立图层:
修改 transform
↓
直接更新图层属性
↓
合成
11. 什么是 Paint?
核心回答
Paint 不是直接把像素画到屏幕上,而是生成"怎么画"的绘制指令。
例如:
text
Paint
↓
画背景
画边框
画文字
画阴影
画图片
...
↓
绘制指令
这是一个非常容易被忽略的细节。所以不要简单说:
Paint = 把东西画到屏幕。
更准确的是:
Paint 生成绘制指令,后续再
由光栅化把这些指令转换成像素。
12. 什么是分块?
核心回答
分块就是把需要光栅化的内容拆成一个个 tile,方便浏览器按需调度和处理。
可以理解成:
text
一个很大的页面
┌────┬────┬────┐
│tile│tile│tile│
├────┼────┼────┤
│tile│tile│tile│
├────┼────┼────┤
│tile│tile│tile│
└────┴────┴────┘
为什么要这么做?因为页面可能非常大。没必要每次都把整个页面一次性光栅化。
特别是滚动页面:
text
当前可视区域
↓
优先处理附近需要显示的 tiles
13. 什么是光栅化?
核心回答
光栅化就是把 Paint 阶段产生的绘制指令真正转换成像素数据。
例如:
text
Paint
↓
"这里画一个红色矩形"
"这里画一段文字"
"这里画一张图片"
↓
Raster
↓
像素
所以可以把两者区别记成:
text
Paint
= 告诉浏览器"怎么画"
Raster
= 真正把"怎么画"变成像素
14. 什么是 Composite?
核心回答
Composite 就是把已经准备好的不同图层/图块按照位置、层级、透明度、transform 等信息合成最终画面。
例如:
text
Layer A
↓
Layer B
↓
Layer C
↓
Composite
↓
最终画面
如果一个元素已经有独立图层:
css
.box {
transform: translateX(100px);
}
那么动画过程中可能主要改变的是:
text
图层位置
↓
Composite
而不需要每一帧重新:
text
Layout
↓
Paint
↓
Raster
这就是合成动画通常性能比较好的原因。
15. 修改 width 会发生什么?
核心回答
修改 width 会影响元素几何信息,所以通常需要重新布局,并可能进一步触发绘制和光栅化,最后重新合成。
典型路径:
text
修改 width
↓
Layout
↓
Paint
↓
Raster
↓
Composite
注意:
这是典型路径,不是绝对规则。
实际浏览器会根据具体元素、渲染状态、优化策略决定哪些阶段需要重新执行。
16. 修改 color 呢?
核心回答
修改 color 通常不需要重新布局,但会影响绘制,所以通常需要重新 Paint,后续相关内容还需要重新光栅化并合成。
大致:
text
color
↓
Paint
↓
Raster
↓
Composite
17. 为什么 transform 通常性能更好?
核心回答
transform 只改变合成层的变换矩阵,不触发重排和重绘,直接交给 GPU 处理。
例如:
css
.box {
transform: translateX(100px);
}
可能变成:
text
原来的页面
↓
Layout
↓
Paint
↓
Raster
↓
独立图层
↓
transform变化
↓
Composite
动画过程中主要处理:
text
Composite
而不是反复:
text
Layout → Paint → Raster
18. transform 是不是一定不走主线程?
核心回答
不是。不能把 transform 简单理解成"完全不走主线程",准确说法是:满足条件时,它可以让动画主要在合成阶段处理。
transform 和 opacity 在满足合成条件时,可以避免每帧重新执行 Layout 和 Paint,从而降低主线程压力。
19. 一帧之内修改 10 次样式,会 Layout 10 次吗?
核心回答
不一定。浏览器通常会把连续的样式修改合并,在合适的渲染时机统一处理。
例如:
js
box.style.width = '100px';
box.style.width = '200px';
box.style.width = '300px';
通常没必要:
text
100px → Layout
200px → Layout
300px → Layout
而可能最终只处理:
text
100px
↓
200px
↓
300px
↓
最终300px
↓
Layout
所以:
不要把每次 DOM/CSS 修改都等同于一次 Layout。
20. 什么情况下会强制浏览器立即 Layout?
核心回答
改完样式马上读布局值,浏览器为了给出最新结果,会先强制更新布局。
例如:
js
box.style.width = '500px';
console.log(box.offsetWidth);
因为:
js
offsetWidth
需要最新布局结果。浏览器可能:
text
修改样式
↓
布局失效
↓
读取 offsetWidth
↓
强制 Layout
↓
返回结果
这就是性能问题的来源之一。
21. 什么是 Layout Thrashing?
核心回答
就是不断交替"修改布局"和"读取布局",导致浏览器反复被迫同步 Layout。
例如:
js
for (const item of items) {
item.style.width = '100px';
console.log(item.offsetWidth);
}
可能形成:
text
Write
↓
Read
↓
Layout
Write
↓
Read
↓
Layout
Write
↓
Read
↓
Layout
大量重复计算就会拖慢页面。
更合理:
js
for (const item of items) {
item.style.width = '100px';
}
for (const item of items) {
console.log(item.offsetWidth);
}
尽量变成:
text
Write
Write
Write
Write
↓
Read
Read
Read
Read
22. requestAnimationFrame 在这里解决什么问题?
核心回答
requestAnimationFrame 会把回调安排到浏览器下一次绘制前执行,这样动画更新就能跟着浏览器的绘制节奏走。
例如:
js
requestAnimationFrame(() => {
box.style.transform = `translateX(${x}px)`;
});
但不要把 requestAnimationFrame 理解成:
"只要用了它,就不会回流。"
它解决的是:
更新时机和渲染节奏的问题。
23. will-change 是不是 GPU 加速开关?
核心回答
不是。will-change 是提前告诉浏览器某个元素的哪些属性可能发生变化,浏览器可以提前做优化准备。
例如:
css
.box {
will-change: transform;
}
它不是:
text
will-change
↓
强制GPU加速
而是:
text
告诉浏览器:
这个元素接下来可能发生 transform 变化
↓
浏览器自行决定是否提前优化
所以不能滥用。因为独立图层本身也有:
text
GPU/内存占用
纹理管理成本
合成成本
图层不是越多越好。
24. contain: layout 能解决什么问题?
核心回答
它可以把布局影响限制在特定元素内部,减少内部变化向外部扩散的范围。
例如:
css
.card {
contain: layout;
}
重点是:
限制布局影响范围。
大型列表、复杂组件、局部独立区域中比较有价值。
25. 首屏图片很多,白屏时间长,应该怎么排查?
核心回答
我不会看到图片多就直接压图片,而是先用 Performance 和 Network 定位白屏到底卡在网络、HTML/CSS、JS,还是主线程渲染,再针对瓶颈处理。
例如:
text
白屏
↓
到底卡在哪里?
│
├─ TTFB 高?
│
├─ HTML 下载慢?
│
├─ CSS 下载/解析慢?
│
├─ JS 下载慢?
│
├─ JS 执行时间长?
│
├─ 首屏图片请求晚?
│
├─ 主线程任务太重?
│
└─ Layout / Paint 太重?
这才是线上问题的正确排查思路。
26. 如果发现 CSS 文件特别大,应该怎么办?
核心回答
首屏用到的 CSS 先加载,首屏用不到的 CSS 后加载。
可以考虑:
text
大 CSS
↓
拆分
↓
关键 CSS
↓
优先加载
非首屏样式根据场景延后。比如不同媒体类型的 CSS:
html
<link
rel="stylesheet"
href="print.css"
media="print"
>
核心思想就是:
减少关键渲染路径上的阻塞。
27. 如果首屏图片特别多,应该怎么优化?
核心回答
核心不是"所有图片都加载得更快",而是让首屏真正需要的图片优先加载,非首屏图片延后加载。
例如:
text
首屏图片
↓
合理尺寸
↓
压缩
↓
现代图片格式
↓
优先请求
非首屏图片
↓
loading="lazy"
↓
需要时再加载
还可以结合:
html
<img
src="hero.avif"
fetchpriority="high"
alt=""
>
以及:
html
<img
src="below-fold.avif"
loading="lazy"
alt=""
>
28. 这道题真正考什么?
核心回答
它考的不是你能不能背出渲染流程,而是你能不能根据浏览器的渲染管线,判断一次操作会影响哪一步,以及线上性能问题到底卡在哪里。
可以把能力分成四层:
text
第一层
知道流程
↓
HTML → DOM
CSS → CSSOM
↓
布局 → 分层 → 绘制 → 分块 → 光栅化 → 合成
第二层
知道阻塞关系
↓
script 是否阻塞 HTML?
CSS 是否影响渲染?
defer / async 怎么工作?
第三层
知道更新路径
↓
width
→ Layout → Paint → Raster → Composite
color
→ Paint → Raster → Composite
transform
→ 满足条件时主要走 Composite
第四层
能解决线上问题
↓
为什么白屏?
为什么滚动卡?
为什么动画掉帧?
为什么 Layout 很高?
为什么 JS 执行把渲染卡住?
到底应该优化哪一步?
最后:真正面试时怎么答
如果面试官只问:
"说一下浏览器的渲染过程。"
不要一上来讲五分钟。
核心回答
浏览器会先解析 HTML 和 CSS,生成 DOM 和 CSSOM,然后进行样式计算和布局,生成布局树;接着进行分层、绘制、分块和光栅化,最后把这些图层合成并显示到屏幕上。
如果面试官继续追问:
"CSS、JS 谁会阻塞谁?"
再展开:
普通同步 JS 会暂停 HTML 解析;CSS 通常不会直接阻塞 HTML 解析,但关键 CSS 会影响首次渲染,而且 CSS 的加载还可能影响某些脚本的执行。
再追问:
"为什么 transform 动画性能好?"
再答:
因为满足合成条件时,transform 可以直接作用在已经准备好的图层上,动画过程中主要走合成,不需要每一帧重新做布局和绘制。
再追问:
"一帧改十次样式,会 Layout 十次吗?"
再答:
不一定,浏览器通常会把连续修改合并;但如果修改后马上读取 offsetWidth 这类布局信息,就可能强制同步 Layout,连续这样做就容易产生 Layout Thrashing。
这才是这道题从**"会背流程"到"真正懂浏览器渲染"**的分水岭。