一、浏览器渲染流程
解析 HTML → 解析 CSS → 样式计算 → 布局 → 分层 → 绘制 → 光栅化 → 合成
-
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);
- 基于渲染树,计算每个元素的几何属性——包括元素的位置(left、top、right、bottom)、大小(width、height、padding、margin)、以及元素之间的关系。
- 遵循盒模型规则,结合视口大小、父节点布局约束完成计算;
-
生成布局树:布局树仅包含可见节点的几何布局信息,与 DOM 树结构不完全一致(如
display: none节点被剔除)。
- 布局是“自上而下”的:从根节点开始,依次计算每个子节点的几何属性,因为父元素的大小和位置会影响子元素。
- 布局是“流式布局”:浏览器会按照文档流的顺序,依次计算元素的位置,一旦计算完成,就会确定元素在页面中的最终位置(除非后续触发回流)。
3.2 关键特性
- 回流(Reflow) :布局计算是递归的,子节点布局变化会触发父节点重新计算,开销极大;
- 布局抖动(Layout Thrashing) :频繁读取 + 修改布局属性(如
offsetTop+style.top)会强制浏览器反复计算布局,导致性能暴跌。
当修改了节点的几何属性,如大小、位置,就需要重新计算布局,这个过程也叫做回流或者重排(reflow)
获取节点的几何属性时,如 offsetWidth / getBoundingClientRect/clientWidth 会强制重排
-
分层
- 主线程会使用一套复杂的策略对整个布局树中进行分层。
- 将页面进行分层,之后某个层变化时,就可以单独更新这一个图层,从而避免了全页面的更新,提高效率。
- 滚动条、堆叠上下文、
transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。
-
绘制
绘制阶段输出:在坐标(100,100)画一个宽200px、高100px、背景红色的矩形(指令);
绘制阶段为每个图层生成绘制指令列表,定义 “先画什么、后画什么”(如先画背景,再画边框,最后画文本)。
过程:
- 将布局信息转换为屏幕上的像素
- 填充颜色、绘制边框、渲染文本等
绘制顺序:
- 背景色
- 背景图片
- 边框
- 文本
- 子元素(按 z-index 和 DOM 顺序)
关键点:
- 绘制是 分层 的:不同层可以独立绘制
- 合成层优化:某些元素会被提升到合成层,独立绘制
核心流程
- 渲染引擎将图层的绘制过程拆解为原子化指令(如 “绘制矩形”“绘制文本”“绘制渐变”);
- 按绘制顺序组合指令生成绘制列表(Paint List);
- 主线程将绘制列表提交给合成线程。
性能关键点
- 绘制指令越复杂(如多层阴影、渐变),绘制耗时越长;
- 避免给大尺寸图层添加复杂绘制属性(如
box-shadow)。
-
光栅化
光栅化阶段输出:把这个指令转换成「100×200 个红色像素点组成的矩阵」(位图)
光栅化 = 把"画什么"变成"真正的像素"
光栅化是渲染流程中 “绘制” 之后、“合成” 之前的核心步骤:绘制阶段生成抽象的绘制指令,光栅化把这些指令转换成 GPU 能识别的位图(像素矩阵),并通过分块(Tiling)优化大图层的处理效率;光栅化的耗时取决于图层大小和绘制复杂度,前端优化需尽量简化绘制指令、控制图层范围,利用 GPU 加速提升光栅化效率。
分块
浏览器会把图层切成小块(通常是256x256像素),分别进行光栅化
-
光栅化:浏览器会把图层切成小块(通常是256x256像素),分别进行光栅化
-
GPU加速:现代浏览器会用GPU来做光栅化,速度更快
-
延迟光栅化:只光栅化当前屏幕可见的部分,滚动时才光栅化新区域
上面我们已经获得了文档结构、元素的样式、元素的几何关系、绘画顺序,接下来把这些信息转化为显示器中的像素才能显示,这个转化的过程,就叫做光栅化。此过程是合成器的光栅工作线程把每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息
合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图。
-
合成
合成(Composite)是浏览器把多个「光栅化后的 图层 位图 」,按层叠顺序合并成最终屏幕画面的过程—— 就像用 GPU 把不同的 “像素乐高块” 拼在一起,输出到屏幕上。
把已经画好的多张“透明胶片”(图层位图),按照正确的顺序叠在一起,最终生成一帧完整的图像送给屏幕显示。
如何优化动画性能?
A: 尽量让动画触发 合成 (Composite) 阶段,而不是 布局 (Layout) 或 绘制 (Paint) 阶段。
- ✅ 推荐:使用
transform(位移/缩放/旋转) 和opacity。它们只影响合成,GPU 处理极快。 - ❌ 避免:使用
top,left,width,height,margin。它们会触发布局重算,甚至重绘,导致卡顿。
Q: 什么是“合成器卡顿” (Compositor Jank)?
A: 即使使用了合成,如果图层太多、太大(显存爆炸),或者图层间依赖关系太复杂(导致无法并行合成),GPU 也会忙不过来,导致掉帧。
GPU 硬件加速的原理有两个原因:
-
使用某些css属性后,渲染引擎会把该把元素单独分层交给 GPU 渲染,GPU处理图形计算更快;
-
分层渲染不会造成页面的回流重绘;
-
CPU:负责计算布局、逻辑、DOM(累、忙、容易卡)
-
GPU:专门负责画图、移动、旋转、缩放(快、丝滑、不占主线程)
GPU 加速 = 把元素交给 GPU 单独渲染,不走主线程,不回流、不重绘,动画超级流畅。
为什么要用 GPU 加速?
因为浏览器渲染有三阶段:
- Layout(回流 / 重排) → 最贵
- Paint(重绘) → 中等
- Composite(合成) → 最便宜, GPU 干的
普通修改(top/left/width)会触发:回流 → 重绘 → 合成(巨卡)
GPU 加速只触发:合成(丝滑 60fps)
哪些属性能用 GPU 加速?
只有两个:
- transform
- opacity
这两个修改,浏览器会:
- 给元素单独开一个 图层
- 交给 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、频繁读取布局属性、图片未设置宽高、窗口大小频繁变化。
渲染优化实战技巧
结合前面的原理和解决方案,我们总结一些实际开发中可以直接使用的渲染优化技巧,提升页面加载速度和流畅度。
- 优化首屏加载:
- 内联关键CSS,异步加载非关键CSS。
- 异步加载非关键JS,将关键JS内联或放在末尾。
- 压缩HTML、CSS、JS文件,减少文件体积(使用Gzip/Brotli压缩)。
- 优化布局和绘制:
- 避免使用table布局(table布局会触发多次重排)。
- 避免频繁读取和修改DOM属性(如offsetWidth、scrollTop),尽量一次性读取、一次性修改。
- 使用CSS硬件加速(transform、opacity)实现动画,避免使用width、height、left、top等属性。
- 优化合成层:
- 给动画元素添加will-change: transform;,提升合成效率。
- 避免过多的合成层(如不要给所有元素都添加transform: translateZ(0);)。
- 其他优化:
- 预加载关键资源:使用预加载首屏必需的图片、CSS、JS。
- 懒加载非首屏资源:图片、视频等资源使用懒加载(loading="lazy"),避免占用首屏加载带宽。
- 避免使用display: none;(会触发重排),如需隐藏元素,可使用visibility: hidden;(只触发重绘)或opacity: 0;(不触发重排/重绘)。
性能优化实践
-
减少重排和重绘
批量 DOM 操作
❌ 错误做法:
// 每次操作都触发重排
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // 触发 100 次重排
}
✅ 正确做法:
// 使用 DocumentFragment 批量操作
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
list.appendChild(fragment); // 只触发 1 次重排
读写分离
❌ 错误做法:
// 强制同步布局(Layout Thrashing)
const elements = document.querySelectorAll('.item');
for (let i = 0; i < elements.length; i++) {
const width = elements[i].offsetWidth; // 读:触发重排
elements[i].style.width = width + 10 + 'px'; // 写:触发重排
}
✅ 正确做法:
// 先读后写,批量操作
const elements = document.querySelectorAll('.item');
const widths = [];
// 批量读取
for (let i = 0; i < elements.length; i++) {
widths[i] = elements[i].offsetWidth;
}
// 批量写入
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px';
}
使用 transform 替代位置属性
❌ 错误做法:
// 使用 top/left 会触发重排
element.style.top = '100px';
element.style.left = '200px';
✅ 正确做法:
// 使用 transform 只触发合成,不触发重排
element.style.transform = 'translate(200px, 100px)';
2. ### 优化 CSS 选择器
选择器性能对比
/* ❌ 性能差:从右到左匹配,需要遍历所有 div */
div.container .item .sub-item {
color: red;
}
/* ✅ 性能好:直接匹配类名 */
.sub-item {
color: red;
}
/* ❌ 性能差:通配符匹配所有元素 */
* {
margin: 0;
padding: 0;
}
/* ✅ 性能好:明确指定元素 */
body, h1, h2, p {
margin: 0;
padding: 0;
}
选择器复杂度
选择器复杂度计算:
- 标签选择器:1
- 类选择器:10
- ID 选择器:100
- 内联样式:1000
优化建议:
- 避免深层嵌套(超过 3 层)
- 优先使用类选择器
- 避免使用属性选择器(如
[data-*])进行频繁匹配
-
使用合成层优化动画
创建合成层
/* 方法1:使用 transform */
.animated-element {
transform: translateZ(0);
/* 或 */
transform: translate3d(0, 0, 0);
}
/* 方法2:使用 will-change 提示 */
.animated-element {
will-change: transform;
}
/* 方法3:使用 opacity(配合动画) */
.fade-element {
opacity: 0.99;
transition: opacity 0.3s;
}
动画性能对比
/* ❌ 性能差:触发重排和重绘 */
@keyframes slide {
from { left: 0; }
to { left: 100px; }
}
/* ✅ 性能好:只触发合成 */
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
4. ### 优化关键渲染路径
内联关键 CSS
<head>
<!-- 关键 CSS 内联,减少请求 -->
<style>
body { font-family: Arial; margin: 0; }
.header { height: 60px; background: #fff; }
.main { padding: 20px; }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
</head>
延迟加载非关键资源
<!-- 延迟加载图片 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="图片">
<!-- 延迟加载字体 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
使用 Resource Hints
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="critical.css" as="style">
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="next-page.html">
5. ### 虚拟滚动优化长列表
问题: 渲染大量 DOM 节点会导致性能问题
解决方案: 只渲染可见区域的元素
class VirtualScroll {
constructor(container, itemHeight, items) {
this.container = container;
this.itemHeight = itemHeight;
this.items = items;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
this.scrollTop = 0;
this.init();
}
init() {
this.container.addEventListener('scroll', () => {
this.scrollTop = this.container.scrollTop;
this.render();
});
this.render();
}
render() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleCount + 1;
const visibleItems = this.items.slice(startIndex, endIndex);
// 只渲染可见元素
this.container.innerHTML = visibleItems.map((item, index) => {
const actualIndex = startIndex + index;
return `<div style="height: ${this.itemHeight}px; position: absolute; top: ${actualIndex * this.itemHeight}px;">${item}</div>`;
}).join('');
// 设置总高度,保持滚动条正确
this.container.style.height = `${this.items.length * this.itemHeight}px`;
}
}
常见问题与解决方案
-
白屏问题
问题现象: 页面长时间显示空白
可能原因:
- CSS 阻塞渲染
- JavaScript 执行时间过长
- 资源加载失败
解决方案:
<!-- 1. 内联关键 CSS -->
<style>
/* 首屏关键样式 */
body { font-family: Arial; }
.loading { display: flex; justify-content: center; }
</style>
<!-- 2. 使用 defer 延迟非关键 JS -->
<script src="app.js" defer></script>
<!-- 3. 添加加载提示 -->
<div class="loading">加载中...</div>
2. ### 布局抖动 (Layout Shift)
问题现象: 页面元素突然移动,影响用户体验
原因: 异步加载的资源(如图片、字体)导致布局变化
解决方案:
<!-- 1. 为图片设置尺寸 -->
<img src="image.jpg" width="300" height="200" alt="图片">
<!-- 2. 使用 aspect-ratio 保持比例 -->
<div style="aspect-ratio: 16/9;">
<img src="image.jpg" alt="图片">
</div>
<!-- 3. 预留占位空间 -->
<div style="height: 200px; background: #f0f0f0;">
<img src="image.jpg" alt="图片">
</div>
<!-- 4. 使用 font-display 控制字体加载 -->
<style>
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
font-display: swap; /* 显示备用字体,字体加载后替换 */
}
</style>
3. ### 强制同步布局 (Layout Thrashing)
问题现象: 频繁读取布局属性导致性能问题
示例:
// ❌ 问题代码
function resizeItems() {
const items = document.querySelectorAll('.item');
items.forEach(item => {
const width = item.offsetWidth; // 读:触发重排
item.style.width = width * 2 + 'px'; // 写:触发重排
});
}
解决方案:
// ✅ 优化代码
function resizeItems() {
const items = document.querySelectorAll('.item');
const widths = [];
// 批量读取
items.forEach((item, index) => {
widths[index] = item.offsetWidth;
});
// 批量写入
items.forEach((item, index) => {
item.style.width = widths[index] * 2 + 'px';
});
}
4. ### 内存泄漏
问题现象: 页面长时间运行后变慢
常见原因:
- 事件监听器未移除
- 定时器未清除
- DOM 引用未释放
解决方案:
// ✅ 正确的事件监听管理
class Component {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
handleResize() {
// 处理逻辑
}
destroy() {
// 清理事件监听
window.removeEventListener('resize', this.handleResize);
}
}
// ✅ 正确的定时器管理
class TimerManager {
constructor() {
this.timers = [];
}
setTimeout(callback, delay) {
const timer = setTimeout(callback, delay);
this.timers.push(timer);
return timer;
}
clearAll() {
this.timers.forEach(timer => clearTimeout(timer));
this.timers = [];
}
}
5. ### 长列表性能问题
问题现象: 渲染大量列表项时页面卡顿
解决方案:
- 使用虚拟滚动(只渲染可见项)
- 使用
content-visibilityCSS 属性 - 分页加载
/* 使用 content-visibility 优化 */
.list-item {
content-visibility: auto;
contain-intrinsic-size: 200px; /* 预估高度 */
}
实战案例分析
案例1:优化首屏渲染时间
场景: 电商首页加载缓慢
问题分析:
- CSS 文件过大(200KB)
- 关键 CSS 未内联
- JavaScript 阻塞渲染
- 图片未优化
优化方案:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. 内联关键 CSS(首屏样式) -->
<style>
body { font-family: Arial; margin: 0; }
.header { height: 60px; background: #fff; position: fixed; top: 0; width: 100%; }
.banner { height: 400px; background: #f0f0f0; margin-top: 60px; }
.loading { display: flex; justify-content: center; align-items: center; height: 200px; }
</style>
<!-- 2. 预加载关键资源 -->
<link rel="preload" href="logo.png" as="image">
<link rel="preload" href="critical.js" as="script">
<!-- 3. DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
</head>
<body>
<!-- 4. 首屏内容优先 -->
<div class="header">头部导航</div>
<div class="banner">轮播图</div>
<!-- 5. 非关键内容延迟加载 -->
<div id="product-list" class="loading">加载中...</div>
<!-- 6. 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
<!-- 7. JavaScript 延迟执行 -->
<script src="critical.js" defer></script>
<script>
// 延迟加载非关键 JS
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'non-critical.js';
document.body.appendChild(script);
});
</script>
</body>
</html>
优化效果:
- 首屏渲染时间:从 3.5s 降低到 1.2s
- FCP (First Contentful Paint):从 2.8s 降低到 0.9s
- LCP (Largest Contentful Paint):从 4.2s 降低到 1.5s
案例2:优化动画性能
场景: 页面滚动时元素跟随动画卡顿
问题代码:
// ❌ 使用 scroll 事件 + top/left
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset;
element.style.top = scrollTop + 100 + 'px'; // 触发重排
element.style.left = scrollTop + 50 + 'px'; // 触发重排
});
优化代码:
// ✅ 使用 transform + requestAnimationFrame
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
const scrollTop = window.pageYOffset;
element.style.transform = `translate(${scrollTop + 50}px, ${scrollTop + 100}px)`;
ticking = false;
});
ticking = true;
}
});
进一步优化:
/* 创建合成层 */
.follow-element {
will-change: transform;
transform: translateZ(0);
}
优化效果:
- 帧率:从 30fps 提升到 60fps
- CPU 使用率:降低 40%
案例3:优化表格渲染性能
场景: 渲染 10000 行数据表格卡顿
问题代码:
// ❌ 一次性渲染所有行
function renderTable(data) {
const tbody = document.querySelector('tbody');
tbody.innerHTML = data.map(item => `
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.email}</td>
</tr>
`).join('');
}
优化方案1:虚拟滚动
class VirtualTable {
constructor(container, data, rowHeight = 50) {
this.container = container;
this.data = data;
this.rowHeight = rowHeight;
this.visibleCount = Math.ceil(container.clientHeight / rowHeight);
this.scrollTop = 0;
this.init();
}
init() {
this.container.addEventListener('scroll', () => {
this.scrollTop = this.container.scrollTop;
this.render();
});
this.render();
}
render() {
const startIndex = Math.floor(this.scrollTop / this.rowHeight);
const endIndex = Math.min(startIndex + this.visibleCount + 2, this.data.length);
const visibleData = this.data.slice(startIndex, endIndex);
const offsetY = startIndex * this.rowHeight;
this.container.querySelector('tbody').innerHTML = visibleData.map((item, index) => `
<tr style="position: absolute; top: ${offsetY + index * this.rowHeight}px; height: ${this.rowHeight}px;">
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.email}</td>
</tr>
`).join('');
this.container.style.height = `${this.data.length * this.rowHeight}px`;
}
}
优化方案2:使用 content-visibility
.table-row {
content-visibility: auto;
contain-intrinsic-size: 50px;
}
优化效果:
- 初始渲染时间:从 5s 降低到 0.3s
- 滚动流畅度:从卡顿提升到 60fps
- 内存占用:降低 80%
案例4:优化复杂表单渲染
场景: 包含大量输入框和选择器的表单页面加载慢
问题分析:
- 表单元素过多(500+ 个输入框)
- 每个输入框都有事件监听器
- 样式计算开销大
优化方案:
// ✅ 使用事件委托替代每个元素绑定事件
class FormManager {
constructor(formElement) {
this.form = formElement;
// 只在表单容器上绑定一个事件监听器
this.form.addEventListener('input', this.handleInput.bind(this));
this.form.addEventListener('change', this.handleChange.bind(this));
}
handleInput(e) {
// 通过事件冒泡处理所有输入框
if (e.target.tagName === 'INPUT') {
// 处理逻辑
this.validateField(e.target);
}
}
handleChange(e) {
if (e.target.tagName === 'SELECT') {
// 处理逻辑
this.updateDependentFields(e.target);
}
}
}
// ✅ 使用 CSS contain 属性优化样式计算
.form-section {
contain: layout style paint;
}
优化效果:
- 初始渲染时间:从 2.5s 降低到 0.8s
- 事件绑定时间:从 500ms 降低到 10ms
- 内存占用:降低 60%
案例5:优化图片加载导致的布局抖动
场景: 图片异步加载导致页面元素频繁移动
问题代码:
<!-- ❌ 未设置尺寸,导致布局抖动 -->
<img src="image.jpg" alt="图片">
优化方案:
<!-- ✅ 方案1:设置固定尺寸 -->
<img src="image.jpg" width="300" height="200" alt="图片">
<!-- ✅ 方案2:使用 aspect-ratio -->
<div style="aspect-ratio: 16/9; background: #f0f0f0;">
<img src="image.jpg" alt="图片" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<!-- ✅ 方案3:使用占位符 -->
<div style="width: 300px; height: 200px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: loading 1.5s infinite;">
<img src="image.jpg" alt="图片" style="width: 100%; height: 100%; object-fit: cover;" loading="lazy">
</div>
优化效果:
-
CLS (Cumulative Layout Shift):从 0.25 降低到 0.05
-
用户体验:消除了页面抖动
四、渲染阻塞:为什么 JS 会影响页面加载?
浏览器在渲染过程中,存在「JS 执行阻塞 HTML 解析和 CSSOM 构建」的机制,这是导致页面白屏、加载缓慢的常见原因。
(一)JS 阻塞 HTML 解析
-
原因:JS 可以修改 DOM(如
document.createElement)和 CSSOM(如document.styleSheets),为了保证解析的正确性,浏览器遇到<script>标签时,会:- 暂停 HTML 解析;
- 下载 JS 文件(如果是外部 JS);
- 执行 JS 代码;
- 恢复 HTML 解析。
-
示例:如果
<script>标签放在<body>前,且 JS 执行时间长,会导致页面长时间白屏(HTML 未解析完成,无 DOM 节点可渲染)。
(二)JS 阻塞 CSSOM 构建
- 原因:JS 可以读取和修改 CSSOM(如
getComputedStyle),如果 CSSOM 未构建完成,JS 会等待 CSSOM 构建完成后再执行; - 连锁反应:JS 等待 CSSOM → 阻塞 HTML 解析 → 阻塞渲染,最终导致页面加载延迟。
(三)解决方案:避免渲染阻塞
-
JS 标签优化:
-
外部 JS 放在
<body>底部(确保 HTML 先解析完成); -
用
async/defer属性(异步加载 JS,不阻塞 HTML 解析):async:加载完成后立即执行(执行顺序不确定);defer:加载完成后,等待 HTML 解析完成再执行(执行顺序与标签顺序一致);
-
-
CSS 优化:
- 外部 CSS 用
<link rel="stylesheet">引入(并行下载,不阻塞 HTML 解析,但阻塞渲染); - 避免在
<head>中写大量内联 CSS(增加 CSSOM 构建时间);
- 外部 CSS 用
-
关键 CSS 内联:将首屏渲染必需的 CSS 内联到
<head>中,减少外部 CSS 下载延迟。
五、性能优化:基于渲染原理的实战技巧
优化的核心思路是:减少回流、减少重绘、利用合成加速、避免渲染阻塞。
(一)减少回流(最关键)
-
批量修改 DOM:
- 避免频繁修改单个 DOM 的样式 / 结构,用
DocumentFragment批量操作:
- 避免频繁修改单个 DOM 的样式 / 结构,用
// 优化前(多次回流)
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
list.appendChild(li); // 每次appendChild都触发回流
}
// 优化后(一次回流)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
fragment.appendChild(li); // 不触发回流
}
list.appendChild(fragment); // 仅一次回流
2. 避免读取布局属性后立即修改:
1. 读取`offsetWidth`/`scrollTop`等属性会触发 “强制同步布局”(浏览器需立即计算布局),如果后续修改 DOM,会导致二次回流:
// 优化前(两次回流)
const box = document.getElementById('box');
const width = box.offsetWidth; // 触发布局
box.style.width = `${width + 10}px`; // 再次触发布局
// 优化后(一次回流)
const box = document.getElementById('box');
box.style.width = `${box.offsetWidth + 10}px`; // 合并操作,一次回流
3. 使用 display: none 批量修改:
1. `display: none`的元素不会出现在渲染树中,修改其样式 / 结构不会触发回流,修改完成后恢复`display`:
const box = document.getElementById('box');
box.style.display = 'none'; // 脱离渲染树
box.style.width = '200px'; // 无回流
box.style.height = '200px'; // 无回流
box.style.display = 'block'; // 一次回流
4. 避免 表格布局:表格布局的回流成本极高(一个单元格变化会导致整个表格重新布局),优先用 Flex/Grid 布局。
(二)减少重绘
-
集中修改样式:
- 避免频繁修改单个样式属性,用
class批量修改:
- 避免频繁修改单个样式属性,用
// 优化前(多次重绘)
box.style.color = 'red';
box.style.fontSize = '16px';
box.style.background = '#f5f5f5';
// 优化后(一次重绘)
box.classList.add('active'); // .active { color: red; font-size: 16px; background: #f5f5f5; }
2. 避免使用 visibility: hidden 替代 display: none:
1. `visibility: hidden`的元素仍在渲染树中,修改其样式会触发重绘;`display: none`的元素脱离渲染树,无重绘。
(三)利用合成加速
-
动画用
transform和opacity:transform(如translate、scale)和opacity的修改仅触发合成,不触发回流和重绘,是性能最优的动画实现方式:
/* 优化前(触发回流+重绘) */
.box {
position: absolute;
left: 0;
transition: left 0.3s;
}
.box:hover { left: 100px; }
/* 优化后(仅触发合成) */
.box {
transition: transform 0.3s;
}
.box:hover { transform: translateX(100px); }
2. 创建独立 图层:
1. 对频繁动画的元素,用`will-change`提示浏览器创建独立图层,提前做好优化准备:
.box { will-change: transform; }
(四)避免渲染阻塞
-
JS 优化:
- 首屏非必需的 JS(如统计、广告)用
async/defer加载; - 大型 JS 文件用代码分割(Code Splitting),按需加载;
- 首屏非必需的 JS(如统计、广告)用
-
CSS 优化:
- 外部 CSS 文件放在
<head>中(确保样式优先加载,避免 “无样式内容闪烁(FOUC)”); - 非首屏 CSS 用媒体查询
media="print"等,不阻塞首屏渲染:
- 外部 CSS 文件放在
<link rel="stylesheet" href="print.css" media="print"> <!-- 仅打印时加载,不阻塞首屏 -->