浏览器渲染原理
面试回答: 从输入 URL 到页面显示,浏览器会先解析 URL,检查协议、缓存、HSTS、Service Worker 等导航条件;没有命中缓存时进行 DNS 解析,拿到 IP 后建立 TCP 或 QUIC 连接,HTTPS 还会完成 TLS 握手。随后浏览器构造 HTTP 请求,服务端或 CDN 返回响应。网络线程收到 HTML 后,把渲染任务交给渲染主线程;主线程解析 HTML 和 CSS,构建 DOM 与 CSSOM,经过样式计算、布局、分层和绘制,把绘制指令提交给合成线程。合成线程再分块、光栅化并合成图层,最终由 GPU 把像素显示到屏幕上。页面更新时不一定重走全部流程:尺寸或位置变化需要重新布局,颜色和阴影等视觉变化通常从绘制开始;独立合成层上的
transform、opacity动画则可能只需要重新合成。
1. 从输入 URL 到收到 HTML
渲染通常不是从“浏览器已经拿到 HTML”才开始讲。一次完整页面导航前面还包括 URL、DNS、连接、请求和响应。
(1) 解析 URL
用户在地址栏输入内容后,浏览器会先判断它是 URL 还是搜索关键词。如果是 URL,会解析协议、主机名、端口、路径、查询参数和片段。
常见处理包括:
- 检查协议是否合法,例如
http:、https:、file:等; - 没有显式协议时,结合浏览器策略、历史访问记录和 HSTS 尝试补全;
- 对空格、中文等不能直接出现在 URL 中的字符进行编码;
- 判断
#fragment是否只是同文档定位,片段通常不会发送给服务器; - 检查浏览器缓存、Service Worker、HSTS、CSP、混合内容策略等导航条件。
(2) DNS 解析
如果目标是域名,浏览器需要把域名解析成 IP 地址。通常会先查缓存,再向递归 DNS 发起查询:
严格说,客户端通常把递归查询交给本地递归 DNS;递归 DNS 在没有缓存时,再通过迭代查询逐级找到权威 DNS。真实场景还可能受到 CDN 调度、DoH/DoT、IPv4/IPv6 选择和 DNS 记录 TTL 的影响。
(3) 建立连接
拿到 IP 后,浏览器会选择目标端口并建立传输连接。HTTP/1.1 和 HTTP/2 通常基于 TCP;如果没有可复用连接,需要先进行 TCP 三次握手:
三次握手用于确认双方收发能力、同步初始序列号,并降低历史旧报文干扰新连接的风险。
如果访问的是 HTTPS,TCP 连接建立后还要进行 TLS 握手。TLS 会协商协议版本和加密套件、验证服务器证书、完成密钥交换,并生成后续 HTTP 数据传输使用的对称密钥。HTTP/2 还可能通过 ALPN 在 TLS 握手中完成应用层协议协商。
HTTP/3 的底层不同,它基于 QUIC/UDP,并把传输连接和 TLS 1.3 握手结合起来;面试回答里可以先讲 TCP + TLS 主链路,再补充 HTTP/3 是现代分支。
(4) 发送 HTTP 请求
连接准备好后,浏览器构造 HTTP 请求并发送给服务器。请求通常包含:
- 请求行:方法、路径和协议版本,例如
GET /index.html HTTP/1.1; - 请求头:
Host、User-Agent、Accept、Cookie、缓存条件等; - 请求体:
POST、PUT、PATCH等方法可能携带表单或 JSON 数据。
请求可能经过浏览器扩展、本地代理、公司网关、CDN、负载均衡和反向代理,最后才到达应用服务器。
(5) 服务端返回响应
服务端或 CDN 收到请求后,可能直接命中缓存,也可能由 Nginx、Apache、Node.js、Tomcat 等服务处理静态资源、鉴权、数据库查询或模板渲染。
响应通常包含:
- 状态行:例如
HTTP/1.1 200 OK; - 响应头:
Content-Type、Cache-Control、ETag、Set-Cookie、Content-Encoding等; - 响应体:HTML、JSON、图片、字体或其他资源内容。
如果返回 301、302、307、308 等重定向状态码,浏览器会根据 Location 重新发起导航。更多完整链路可参考从输入 URL 到页面展示发生了什么。
2. 从 HTML 到屏幕像素
网络线程收到 HTML 文档后,会创建一个渲染任务并放入渲染主线程的消息队列。事件循环取出该任务后,渲染主线程开始执行渲染流水线。
可以用下面这条主线理解:
(1) 构建 DOM
浏览器解析 HTML,将标签、属性和文本转换为 DOM 节点并构建 DOM 树。HTML 解析通常是渐进的,并不是等整个文件下载完成后才开始。
解析开始前或解析过程中,浏览器还会启动预加载扫描器,提前发现 HTML 中的外部 CSS、JS、图片、字体等资源,并按优先级调度下载。这样可以减少主线程真正解析到资源标签时的等待时间。
普通同步脚本可能暂停 HTML 解析,因为脚本可能通过 document.write 或 DOM API 改变当前文档结构。defer、async 和模块脚本的加载与执行时机不同,详见 defer 和 async 的区别。
(2) 构建 CSSOM
浏览器解析样式表,形成 CSSOM,并计算每个元素最终命中的样式规则。
CSS 通常不会阻止 HTML 继续解析,但会阻塞页面渲染;某些同步脚本还需要等待前面的样式表加载完成,因为脚本可能读取元素的计算样式。也就是说,“CSS 不阻塞 HTML 解析”和“CSS 会阻塞渲染”并不矛盾。
浏览器默认样式、外部样式、内部样式和行内样式都会参与 CSSOM 和后续样式计算。
(3) 样式计算
主线程会遍历 DOM 树,计算每个节点最终使用的样式,也就是 Computed Style。
这个过程中,很多值会被规范化:
- 颜色关键字会变成具体颜色值,例如
red变成rgb(255, 0, 0); - 相对单位会结合上下文转换,例如
em、rem、百分比等在特定属性上会得到可计算结果; - 继承、层叠、优先级和默认值会被合并成元素最终样式。
(4) 布局(Layout)
布局阶段根据视口、盒模型和布局规则,计算元素的尺寸与位置。历史资料常把重新执行布局称为 Reflow 或回流。
布局阶段会生成布局树,布局树和 DOM 树并不总是一一对应:
display: none的元素不生成布局盒,也不占据空间;visibility: hidden的元素仍然参与布局,只是不绘制可见内容;- 伪元素、匿名盒等渲染结构不一定与 DOM 节点一一对应。
因此,“渲染树就是 DOM 和 CSSOM 简单合并”只是便于入门的简化说法。
布局可能只影响局部子树,也可能沿父子关系扩散。影响范围取决于布局模型、元素依赖关系和浏览器实现,并非每次都重新计算整张页面。
(5) 分层(Layer)
布局之后,浏览器会根据布局结果、样式和滚动等信息决定如何分层。分层的价值是让后续某些变化可以局部处理,减少无关区域的绘制与合成成本。
可能影响分层的因素包括:
- 堆叠上下文;
- 滚动容器和固定定位;
transform、opacity、filter等属性;<video>、canvas、插件等特殊内容;will-change等优化提示。
分层不是越多越好。独立图层需要额外内存、纹理上传和合成管理成本。
(6) 绘制(Paint)
绘制阶段会为每个图层生成绘制指令集,描述背景、文字、边框、阴影、图片等内容应该如何画出来。绘制指令本身还不是屏幕上的像素,更像是一份“怎么画”的命令列表。
复杂阴影、滤镜、大面积渐变和频繁变化的绘制区域,都可能增加这一阶段的成本。
绘制完成后,主线程会把图层和绘制信息提交给合成线程,后续分块、光栅化和合成主要由合成线程、栅格线程和 GPU 进程协作完成。
(7) 分块与光栅化(Tiling & Raster)
合成线程会把图层划分为更小的图块,这个过程常叫分块或 tiling。分块后,浏览器可以优先处理靠近视口的图块,也可以把工作分发给多个栅格线程。
光栅化会把绘制指令转换成位图。现代浏览器通常会把这部分工作交给 GPU 进程或栅格线程协作完成,最终得到一块一块可以合成的像素纹理。
(8) 合成(Composite)
合成线程拿到每个图层、每个图块的位图后,会生成用于合成的 quad 信息。quad 会描述图块应该出现在屏幕的哪个位置、按什么顺序叠放,以及是否需要应用旋转、缩放、透明度等变换。
合成结果会提交给 GPU 进程,再由 GPU 通过系统图形接口交给硬件显示。transform 的高效之处就在这里:如果元素已经在独立合成层上,位移、缩放、旋转等变换可以在合成线程更新 quad 信息,不必回到渲染主线程重新布局或绘制。
如果元素已经位于合适的独立合成层,修改 transform 或 opacity 往往不需要重新布局和绘制,只需要更新图层的变换或透明度。但是否能够只走合成阶段取决于分层状态和浏览器实现,不能把它理解为无条件规则。
3. 页面变化会从哪个阶段重新开始?
不同属性变化影响的渲染阶段不同:
这张表表达的是常见路径,不是所有浏览器、所有属性组合下的绝对保证。例如修改文字颜色不需要重新布局,但仍可能扩大需要重绘的区域;修改 transform 时,如果元素没有合适的分层,也可能产生额外绘制工作。
4. 回流与重绘
回流(Reflow / Layout)
当元素的结构、尺寸或位置发生变化时,浏览器需要重新计算受影响元素的几何信息。
常见触发来源包括:
- 添加或删除可见 DOM 节点;
- 修改
width、height、margin、padding、border等尺寸属性; - 修改影响布局的位置、字体或行高;
- 文本变化、图片尺寸确定等内容变化;
- 视口尺寸变化;
- 切换
display等改变布局参与状态的属性。
重绘(Repaint)
元素几何信息不变,但视觉外观发生变化时,浏览器通常只需要重新生成或更新绘制内容。
常见例子包括:
color、background-color;box-shadow、border-radius;visibility;- 不影响布局的轮廓和装饰变化。
常见面试结论是:回流通常会带来后续的绘制与合成,重绘则不一定需要重新布局,布局成本通常高于单纯绘制。
这个结论比“回流一定重绘整个页面”更准确,因为现代浏览器会尽量限制无效区域与绘制范围。
5. 强制同步布局为什么危险?
浏览器会尽量把连续的样式修改批量处理,等 JavaScript 任务结束或进入合适的渲染时机后再统一更新布局。
但是,如果代码刚修改了布局相关样式,紧接着又读取依赖最新布局结果的几何信息,浏览器为了返回正确数值,只能先同步完成样式计算和布局:
可能触发布局刷新或样式计算的读取包括:
offsetWidth、offsetHeight、offsetTop、offsetLeft;clientWidth、clientHeight;scrollWidth、scrollHeight、scrollTop;getBoundingClientRect();- 特定情况下的
getComputedStyle()。
读取本身不一定每次都造成布局。如果此前没有让样式或布局失效,浏览器可以直接返回已有结果。真正危险的是写入与读取交替发生。
布局抖动(Layout Thrashing)
在循环中反复“写 → 读”,可能让浏览器每轮都被迫同步布局:
改为先集中读取,再集中写入:
读写分离减少的是强制同步布局次数,并不意味着整个批量更新永远只产生一次局部计算。浏览器最终仍然需要为真实的几何变化执行布局。
更多触发属性和调度示例见布局抖动与强制同步布局。
6. 绘制、合成与图层
为什么 transform 和 opacity 更适合动画?
使用 top、left 或宽高制作动画通常会改变几何信息,需要反复布局。transform 和 opacity 不改变普通文档流中的布局结果,在元素被合适地分层后,可以交给合成阶段处理。
这不代表所有 transform 动画都没有成本:图层首次创建、纹理上传、超大图层光栅化和图层之间的叠加,都可能影响性能。
will-change 为什么不能滥用?
will-change 是对浏览器的优化提示,可能让浏览器提前为变化做准备:
独立图层需要显存和管理成本。给大量列表项长期设置 will-change,可能造成内存增长和合成压力。更合理的做法是在元素即将发生高频变化前使用,并在变化结束后移除。
7. 如何排查渲染性能问题?
先测量,再决定优化位置。
使用 Performance 面板
录制卡顿操作,重点观察:
- 是否存在持续时间过长的 JavaScript 任务;
Recalculate Style和Layout是否频繁出现;- 是否存在大面积或高频
Paint; - 哪段调用栈触发了布局或绘制;
- 帧耗时是否持续超出当前设备的刷新预算。
使用渲染辅助工具
Chrome DevTools 的 Rendering 面板可以辅助观察绘制闪烁、帧率和图层边界。发现问题后,再回到调用栈和具体 DOM 更新定位原因。
常见优化顺序
- 减少不必要的 DOM 更新:避免重复设置相同样式,批量插入节点时使用离线容器或框架批处理能力。
- 分离布局读取与写入:先获取所需几何信息,再统一修改样式。
- 控制单次渲染规模:长列表使用虚拟列表、分页或分批渲染。
- 选择合适的动画属性:位移和透明度动画优先考虑
transform、opacity。 - 在合适时机更新视觉状态:使用
requestAnimationFrame对齐下一次绘制,但不要在同一个回调中继续反复读写布局。 - 谨慎创建图层:只为确有高频变化的元素使用
will-change。
requestAnimationFrame 只能帮助代码与渲染时机对齐,本身不会自动消除长任务或布局抖动。
8. 高频面试题
CSS 为什么通常放在文档头部?
CSSOM 是确定页面样式和布局的必要输入。尽早加载关键 CSS,可以避免页面先以无样式状态出现,再发生明显跳变。
JavaScript 为什么不建议使用阻塞脚本放在头部?
同步脚本会暂停 HTML 解析。现代项目通常使用 defer、模块脚本、代码分割或合理的资源优先级,而不是简单地把所有脚本机械移动到 body 底部。
CSS 会阻塞 HTML 解析吗?
通常不会。HTML 解析主线程遇到外部 CSS 时可以继续向后解析,资源下载和 CSS 解析由浏览器调度并行处理。但 CSSOM 是渲染和布局的重要输入,所以 CSS 会阻塞渲染;同步脚本如果依赖前面的样式,也可能等待 CSS 加载完成。
为什么普通 script 会阻塞 HTML 解析?
普通同步脚本可能通过 DOM API 或 document.write 修改当前文档结构。为了保证脚本执行时看到的是确定的 DOM 状态,浏览器遇到没有 async、defer 或模块语义的外部脚本时,通常会暂停 HTML 解析,等脚本下载并执行完成后再继续。
display: none、visibility: hidden 和 opacity: 0 有什么区别?
读取 offsetWidth 一定会触发回流吗?
不一定。如果布局结果仍然有效,可以直接读取缓存结果;如果前面有尚未处理的布局变更,浏览器才需要先同步完成布局。
为什么回流通常比重绘更贵?
布局需要计算元素间的几何依赖,影响可能沿父子或兄弟关系扩散;绘制主要更新视觉指令和像素。布局完成后通常还要继续绘制和合成,因此整体工作更多。
如何回答“页面为什么卡顿”?
不要直接归因于回流。应依次检查 JavaScript 长任务、样式计算、布局、绘制、图层与资源加载,用 Performance 记录确认真正的瓶颈。
9. 总结
- 从输入 URL 到页面显示,完整链路包括 URL 解析、缓存判断、DNS、连接建立、TLS、HTTP 请求响应和浏览器渲染。
- 首次渲染主线是 DOM/CSSOM、样式计算、布局、分层、绘制、分块、光栅化与合成。
- 几何变化通常从布局开始,纯视觉变化通常从绘制开始,部分合成属性可以只更新图层。
- 回流与重绘不是两个孤立概念,而是页面更新重新进入渲染流水线的不同起点。
- 写入布局样式后立即读取几何信息,会触发强制同步布局;循环交替读写会形成布局抖动。
- 性能优化应以 DevTools 测量结果为依据,避免把所有问题笼统归结为“DOM 太多”或“回流太多”。

