一、浏览器渲染流程
-
HTML文档解析
渲染的第一步是解析 HTML 文档。
解析过程中中遇到HTML元素会解析HTML元素最终生成DOM树,遇到 CSS 会下载并解析 CSS,遇到 JS会暂停解析HTML,而是去下载并执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
因为浏览器无法直接理解和使用html,所以需要将html转换为浏览器能够理解的结构——DOM树。 在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。
生成DOM树
解析的过程中遇到HTML元素会解析HTML元素最终生成DOM树;
生成CSSOM树
解析的过程中遇到style标签、link元素、行内样式等CSS样式,会解析CSS生成CSSOM树。
CSS不会阻塞HTML解析
如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
CSSOM构建:
- 不会阻塞DOM树的构建(现代浏览器)
- 但会阻塞渲染 树 构建(必须等待CSSOM完成)
- 会阻塞后续JavaScript执行(JS可能依赖样式)
JS会阻塞HTML解析
如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM ****树 ,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
-
JavaScript:
- 同步脚本(
<script>)会立即阻塞 DOM 构建 - 遇到脚本时,必须等待当前所有 CSS 下载完成才执行(避免JS操作未解析的样式)
- 同步脚本(
script标签中defer和async的区别
如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。
下图可以直观的看出三者之间的区别:
其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。
其区别如下:
-
执行顺序: 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;
-
async:遇到scirpt标签时,浏览器开始异步下载,下载完成后如果此时 HTML 还没有解析完,浏览器会暂停解析,先让 JS 引擎执行代码,执行完毕后再进行解析(可能会阻塞) -
defer:遇到scirpt标签时,浏览器开始异步下载,html页面解析完才执行js文件。(立即下载,但延迟执行(整个页面都解析完毕之后再执行,不阻塞)
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
-
样式计算
-
CSS 结构化:将外部样式表(
<link>)、内部样式表(<style>)、内联样式(style属性)转换为浏览器可理解的StyleSheet结构; -
属性标准化:将 CSS 属性值转换为标准化格式(如
2em → 32px、blue → rgb(0,0,255)、bold → 700); -
样式 继承 与层叠:
- 继承:子节点继承父节点的可继承属性(如
font-size、color); - 层叠:按 “选择器权重→声明顺序→来源(用户代理样式 < 用户样式 < 内联样式)” 规则解决样式冲突;
- 继承:子节点继承父节点的可继承属性(如
-
计算最终样式:为每个 DOM 节点生成
ComputedStyle(计算样式),存储所有属性的最终值。
性能关键点
- 复杂选择器(如
div > ul li a)会增加样式计算耗时,建议简化选择器(如使用类选择器.link); - CSS 规则匹配是 “从右到左” 的(如
.container .item先匹配.item再匹配.container),避免通配符*和深层嵌套。
-
布局
3.1 核心步骤
布局的核心是计算 DOM 节点的几何属性(位置、大小、边距),生成布局 树 (Layout Tree ) 。
-
构建布局 树:基于 DOM 树,过滤掉不可见节点(如
<head>、display: none的元素),保留可见节点; -
布局计算:
- 从根节点开始,递归计算每个节点的几何属性(x/y 坐标、宽高、margin/padding/border);
- 遵循盒模型规则,结合视口大小、父节点布局约束完成计算;
-
生成布局 树:布局树仅包含可见节点的几何布局信息,与 DOM 树结构不完全一致(如
display: none节点被剔除)。
- 布局是“自上而下”的:从根节点开始,依次计算每个子节点的几何属性,因为父元素的大小和位置会影响子元素。
- 布局是“流式布局”:浏览器会按照文档流的顺序,依次计算元素的位置,一旦计算完成,就会确定元素在页面中的最终位置(除非后续触发回流)。
3.2 关键特性
- 回流(Reflow) :布局计算是递归的,子节点布局变化会触发父节点重新计算,开销极大;
- 布局抖动(Layout Thrashing) :频繁读取 + 修改布局属性(如
offsetTop+style.top)会强制浏览器反复计算布局,导致性能暴跌。
当修改了节点的几何属性,如大小、位置,就需要重新计算布局,这个过程也叫做回流或者重排(reflow)
获取节点的几何属性时,如 offsetWidth / getBoundingClientRect/clientWidth 会强制重排
-
分层
- 主线程会使用一套复杂的策略对整个布局树中进行分层。
- 将页面进行分层,之后某个层变化时,就可以单独更新这一个图层,从而避免了全页面的更新,提高效率。
- 滚动条、堆叠上下文、
transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。
-
绘制
绘制阶段为每个图层生成绘制指令列表,定义 “先画什么、后画什么”(如先画背景,再画边框,最后画文本)。
核心流程
- 渲染引擎将图层的绘制过程拆解为原子化指令(如 “绘制矩形”“绘制文本”“绘制渐变”);
- 按绘制顺序组合指令生成绘制列表(Paint List);
- 主线程将绘制列表提交给合成线程。
性能关键点
- 绘制指令越复杂(如多层阴影、渐变),绘制耗时越长;
- 避免给大尺寸图层添加复杂绘制属性(如
box-shadow)。
-
分块??
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
它会从线程池中拿取多个线程来完成分块工作。
- 大尺寸图层(如全屏滚动区域)会占用大量的GPU内存,消耗资源效率低
- 合成线程会调用多个线程把每个分层都分成更小的块(Tile),通常为256x246或者512x512像素
-
光栅化
上面我们已经获得了文档结构、元素的样式、元素的几何关系、绘画顺序,接下来把这些信息转化为显示器中的像素才能显示,这个转化的过程,就叫做光栅化。此过程是合成器的光栅工作线程把每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息
合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图。
-
画
最后一个阶段就是画了
GPU 硬件加速的原理有两个原因:
-
使用某些css属性后,渲染引擎会把该把元素单独分层交给 GPU 渲染,GPU处理图形计算更快;
-
分层渲染不会造成页面的回流重绘;
-
总结
再让我们来回顾一遍完整过程:
二、重排、重绘与合成
页面交互过程中,JS/CSS 修改会触发渲染流水线的局部更新,按开销从高到低分为三类:
| 类型 | 触发条件 | 涉及渲染阶段 | 性能开销 | 优化优先级 |
|---|---|---|---|---|
| 重排(Reflow) | 几何属性变化:宽高、位置、display、DOM 增删 | 布局→分层→绘制→栅格化→合成 | 极高 | 最高 |
| 重绘(Repaint) | 绘制属性变化:颜色、背景、阴影、边框色 | 绘制→栅格化→合成 | 中等 | 中 |
| 合成(Composite) | 合成属性变化:transform、opacity | 仅合成阶段 | 极低 | 低(优先用) |
4.1 典型触发场景
4.2.1 重排触发场景
- 修改几何属性:
width: 200px、left: 10px、margin: 8px; - 增删 / 移动 DOM 节点:
appendChild、removeChild、insertBefore; - 窗口操作:
resize、scroll(部分浏览器优化了 scroll 的重排); - 读取布局属性:
offsetTop、clientWidth、getComputedStyle(强制浏览器提前完成重排)。
4.2.2 重绘触发场景
- 修改颜色属性:
color: red、background-color: #000; - 修改边框 / 阴影:
border-color: blue、box-shadow: 0 0 10px #000; - 修改文本样式:
text-shadow: 1px 1px 2px #333; - 修改背景:
background-image: url(new.png)、background-position: center。
4.2.3 合成触发场景
transform:translate/scale/rotate/skew;opacity:opacity: 0.5;will-change:提前声明元素即将变化的属性。
三、渲染性能优化:原理与实战
5.1 核心优化原则
- 避免重排,减少重绘,优先合成;
- 减少渲染工作量(精简 DOM、简化样式);
- 利用 GPU 加速(合理创建图层);
- 避免主线程阻塞(优化 JS 执行、减少长任务)。
5.2 分阶段优化方案
5.2.1 DOM 构建阶段优化
- 精简 HTML 结构,减少嵌套层级(如避免超过 6 层嵌套);
- 延迟加载非首屏 JS:
<script defer>/<script async>,避免阻塞 DOM 解析; - 预加载关键资源:
<link rel="preload" href="critical.css">。
5.2.2 样式计算阶段优化
-
简化选择器:用类选择器(
.item)替代标签 / 层级选择器(div > ul li); -
避免通配符:
* { margin: 0 }会遍历所有节点,增加计算耗时; -
抽离通用样式:减少重复样式声明,降低层叠计算复杂度。
底层渲染优化核心逻辑(基于原理的优化方案)
渲染优化的核心,本质是“减少主线程的工作量”和“避免不必要的渲染操作”,结合前面的底层原理,优化方案均对应具体的渲染环节,以下是底层优化的核心逻辑和实操方案。
3.1 优化资源加载(解决阻塞问题)
- 优化 CSS 加载:将关键 CSS 内联到 HTML 头部(减少关键 CSS 加载时间,避免白屏);非关键 CSS 延迟加载(如通过 media="print" 标记,或动态加载);避免 @import 引入 CSS(@import 会阻塞 CSS 解析,且需等待父 CSS 加载完成)。
- 优化 JS 加载:使用 defer/async 属性(避免 JS 阻塞 HTML 解析);将 JS 脚本放在 HTML 底部(或动态加载);拆分 JS 代码(首屏仅加载必需的 JS,非首屏 JS 延迟加载);避免同步加载大型 JS 脚本。
- 优化 图片 加载:使用懒加载(lazyload 属性,或动态加载);使用合适的图片格式(如 WebP、AVIF,减小图片体积);设置图片的宽高属性(避免图片加载完成后,触发布局重排)。
3.2 优化解析过程(减少解析耗时)
- 简化 HTML 结构:减少 DOM 节点数量(避免嵌套过深,如嵌套不超过 6 层);避免无效 HTML 标签(如多余的 div 嵌套);使用语义化标签(如 header、footer,减少不必要的 class)。
- 简化 CSS 选择器:避免复杂的后代选择器、通配符选择器(减少 CSS 解析和样式计算耗时);使用类选择器替代标签选择器、后代选择器;避免过度使用 !important(增加样式优先级计算耗时)。
- 避免 JS 阻塞解析:减少同步 JS 执行时间(避免长时间运算);避免在 JS 中使用 document.write()(修改 HTML 内容,打断解析);合理使用 defer/async。
3.3 优化布局与重绘(减少性能损耗最大的操作)
- 避免频繁触发回流/重绘:
- 批量修改 DOM 和样式(如使用 DocumentFragment 批量添加节点,或先隐藏节点 display: none,修改完成后再显示)。
- 避免频繁读取布局属性(如 offsetWidth、clientHeight、getBoundingClientRect()),若需多次读取,可缓存结果。
- 使用 transform 和 opacity 实现动画(仅触发合成,不触发布局和重绘),替代修改 width、height、top 等几何属性。
优化布局计算:避免使用百分比、em、rem 等依赖父节点几何信息的单位(减少递归计算);固定布局尺寸(如设置固定宽高,避免自适应导致的布局重排);使用 flex/grid 布局(性能优于传统的 float 布局,布局计算更高效)。
优化绘制:减少绘制区域(如使用 contain: paint 属性,限制节点的绘制范围);避免复杂样式(如渐变背景、模糊阴影,可替换为图片);避免频繁修改非几何属性(如 color、background-color)。
3.4 优化合成环节(利用硬件加速)
-
合理使用硬件加速:对需要动画的节点,使用 transform: translateZ(0) 或 will-change: transform 强制分层,触发硬件加速;但需控制合成层数量(避免 GPU 内存不足)。
-
避免合成层爆炸:不要为所有节点强制分层;避免使用过多的 opacity < 1、filter、position: fixed 等属性(会自动生成合成层);定期检查合成层数量(通过 Chrome DevTools 的 Layers 面板)。
-
优化动画性能:动画尽量使用 transform 和 opacity(仅触发合成);避免在动画中修改 DOM 和 CSS(触发布局/重绘);使用 requestAnimationFrame 控制动画(与浏览器渲染帧同步,避免卡顿)。
底层常见问题与排查方法
结合底层渲染原理,日常开发中遇到的渲染卡顿、白屏、样式错乱等问题,都能找到对应的底层原因,以下是常见问题及排查方法(基于 Chrome DevTools)。
4.1 常见问题及底层原因
- 首屏白屏时间过长:底层原因:关键 CSS 加载缓慢、JS 阻塞解析、HTML 解析耗时过长、资源加载优先级不合理。
- 界面 卡顿 (滚动/动画卡顿) :底层原因:主线程被 JS 执行阻塞、频繁触发回流/重绘、合成层过多导致 GPU 内存不足、动画未使用硬件加速。
- 样式错乱:底层原因:CSS 优先级计算错误、样式继承异常、渲染树构建时节点筛选错误、层叠顺序(z-index)设置错误。
- 回流/重绘频繁:底层原因:JS 频繁修改 DOM/CSS、频繁读取布局属性、图片未设置宽高、窗口大小频繁变化。