33

0 阅读24分钟

一、浏览器渲染流程

解析 HTML → 解析 CSS → 样式计算 → 布局 → 分层 → 绘制 → 光栅化 → 合成

  1. 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 树中。

  1. 样式计算

  1. CSS 结构化:将外部样式表(<link>)、内部样式表(<style>)、内联样式(style属性)转换为浏览器可理解的StyleSheet结构;

  2. 属性标准化:将 CSS 属性值转换为标准化格式(如2em → 32pxblue → rgb(0,0,255)bold → 700);

  3. 样式 继承 与层叠

    1. 继承:子节点继承父节点的可继承属性(如font-sizecolor);
    2. 层叠:按 “选择器权重→声明顺序→来源(用户代理样式 < 用户样式 < 内联样式)” 规则解决样式冲突;
  4. 计算最终样式:为每个 DOM 节点生成ComputedStyle(计算样式),存储所有属性的最终值。

性能关键点

  • 复杂选择器(如div > ul li a)会增加样式计算耗时,建议简化选择器(如使用类选择器.link);
  • CSS 规则匹配是 “从右到左” 的(如.container .item先匹配.item再匹配.container),避免通配符*和深层嵌套。

  1. 布局

3.1 核心步骤

布局的核心是计算 DOM 节点的几何属性(位置、大小、边距),生成布局树(Layout Tree)

  1. 构建布局树:基于 DOM 树,过滤掉不可见节点(如<head>display: none的元素),保留可见节点;

  2. 布局计算

    1. 从根节点开始,递归计算每个节点的几何属性(x/y 坐标、宽高、margin/padding/border);
    2. 基于渲染树,计算每个元素的几何属性——包括元素的位置(left、top、right、bottom)、大小(width、height、padding、margin)、以及元素之间的关系。
    3. 遵循盒模型规则,结合视口大小、父节点布局约束完成计算;
  3. 生成布局树:布局树仅包含可见节点的几何布局信息,与 DOM 树结构不完全一致(如display: none节点被剔除)。

  • 布局是“自上而下”的:从根节点开始,依次计算每个子节点的几何属性,因为父元素的大小和位置会影响子元素。
  • 布局是“流式布局”:浏览器会按照文档流的顺序,依次计算元素的位置,一旦计算完成,就会确定元素在页面中的最终位置(除非后续触发回流)。

3.2 关键特性
  • 回流(Reflow) :布局计算是递归的,子节点布局变化会触发父节点重新计算,开销极大;
  • 布局抖动(Layout Thrashing) :频繁读取 + 修改布局属性(如offsetTop+style.top)会强制浏览器反复计算布局,导致性能暴跌。

当修改了节点的几何属性,如大小、位置,就需要重新计算布局,这个过程也叫做回流或者重排(reflow)

获取节点的几何属性时,如 offsetWidth / getBoundingClientRect/clientWidth强制重排

  1. 分层

  • 主线程会使用一套复杂的策略对整个布局树中进行分层。
  • 将页面进行分层,之后某个层变化时,就可以单独更新这一个图层,从而避免了全页面的更新,提高效率。
  • 滚动条、堆叠上下文、transformopacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。

  1. 绘制

绘制阶段输出:在坐标(100,100)画一个宽200px、高100px、背景红色的矩形(指令);

绘制阶段为每个图层生成绘制指令列表,定义 “先画什么、后画什么”(如先画背景,再画边框,最后画文本)。

过程:

  • 将布局信息转换为屏幕上的像素
  • 填充颜色、绘制边框、渲染文本等

绘制顺序:

  1. 背景色
  2. 背景图片
  3. 边框
  4. 文本
  5. 子元素(按 z-index 和 DOM 顺序)

关键点:

  • 绘制是 分层 :不同层可以独立绘制
  • 合成层优化:某些元素会被提升到合成层,独立绘制
核心流程
  1. 渲染引擎将图层的绘制过程拆解为原子化指令(如 “绘制矩形”“绘制文本”“绘制渐变”);
  2. 按绘制顺序组合指令生成绘制列表(Paint List);
  3. 主线程将绘制列表提交给合成线程。
性能关键点
  • 绘制指令越复杂(如多层阴影、渐变),绘制耗时越长;
  • 避免给大尺寸图层添加复杂绘制属性(如box-shadow)。

  1. 光栅化

光栅化阶段输出:把这个指令转换成「100×200 个红色像素点组成的矩阵」(位图)

光栅化 = 把"画什么"变成"真正的像素"

光栅化是渲染流程中 “绘制” 之后、“合成” 之前的核心步骤:绘制阶段生成抽象的绘制指令,光栅化把这些指令转换成 GPU 能识别的位图(像素矩阵),并通过分块(Tiling)优化大图层的处理效率;光栅化的耗时取决于图层大小和绘制复杂度,前端优化需尽量简化绘制指令、控制图层范围,利用 GPU 加速提升光栅化效率。

分块

浏览器会把图层切成小块(通常是256x256像素),分别进行光栅化

  1. 光栅化:浏览器会把图层切成小块(通常是256x256像素),分别进行光栅化

  2. GPU加速:现代浏览器会用GPU来做光栅化,速度更快

  3. 延迟光栅化:只光栅化当前屏幕可见的部分,滚动时才光栅化新区域

上面我们已经获得了文档结构、元素的样式、元素的几何关系、绘画顺序,接下来把这些信息转化为显示器中的像素才能显示,这个转化的过程,就叫做光栅化。此过程是合成器的光栅工作线程把每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息

合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图。

  1. 合成

合成(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 加速?

因为浏览器渲染有三阶段:

  1. Layout(回流 / 重排) → 最贵
  2. Paint(重绘) → 中等
  3. Composite(合成)最便宜, GPU 干的

普通修改(top/left/width)会触发:回流 → 重绘 → 合成(巨卡)

GPU 加速只触发:合成(丝滑 60fps)


哪些属性能用 GPU 加速?

只有两个:

  • transform
  • opacity

这两个修改,浏览器会:

  1. 给元素单独开一个 图层
  2. 交给 GPU 处理
  3. 不触发回流、不触发重绘
  1. 总结

再让我们来回顾一遍完整过程:


二、重排、重绘与合成

页面交互过程中,JS/CSS 修改会触发渲染流水线的局部更新,按开销从高到低分为三类:

类型触发条件涉及渲染阶段性能开销优化优先级
重排(Reflow)几何属性变化:宽高、位置、display、DOM 增删布局→分层→绘制→栅格化→合成极高最高
重绘(Repaint)绘制属性变化:颜色、背景、阴影、边框色绘制→栅格化→合成中等
合成(Composite)合成属性变化:transform、opacity仅合成阶段极低低(优先用)

4.1 典型触发场景

4.2.1 重排触发场景
  • 修改几何属性:width: 200pxleft: 10pxmargin: 8px
  • 增删 / 移动 DOM 节点:appendChildremoveChildinsertBefore
  • 窗口操作:resizescroll(部分浏览器优化了 scroll 的重排);
  • 读取布局属性:offsetTopclientWidthgetComputedStyle(强制浏览器提前完成重排)。
4.2.2 重绘触发场景
  • 修改颜色属性:color: redbackground-color: #000
  • 修改边框 / 阴影:border-color: bluebox-shadow: 0 0 10px #000
  • 修改文本样式:text-shadow: 1px 1px 2px #333
  • 修改背景:background-image: url(new.png)background-position: center
4.2.3 合成触发场景
  • transformtranslate/scale/rotate/skew
  • opacityopacity: 0.5
  • will-change:提前声明元素即将变化的属性。

三、渲染性能优化:原理与实战

5.1 核心优化原则

  1. 避免重排,减少重绘,优先合成
  2. 减少渲染工作量(精简 DOM、简化样式);
  3. 利用 GPU 加速(合理创建图层);
  4. 避免主线程阻塞(优化 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、频繁读取布局属性、图片未设置宽高、窗口大小频繁变化。

渲染优化实战技巧

结合前面的原理和解决方案,我们总结一些实际开发中可以直接使用的渲染优化技巧,提升页面加载速度和流畅度。

  1. 优化首屏加载
  • 内联关键CSS,异步加载非关键CSS。
  • 异步加载非关键JS,将关键JS内联或放在末尾。
  • 压缩HTML、CSS、JS文件,减少文件体积(使用Gzip/Brotli压缩)。
  1. 优化布局和绘制
  • 避免使用table布局(table布局会触发多次重排)。
  • 避免频繁读取和修改DOM属性(如offsetWidth、scrollTop),尽量一次性读取、一次性修改。
  • 使用CSS硬件加速(transform、opacity)实现动画,避免使用width、height、left、top等属性。
  1. 优化合成层
  • 给动画元素添加will-change: transform;,提升合成效率。
  • 避免过多的合成层(如不要给所有元素都添加transform: translateZ(0);)。
  1. 其他优化
  • 预加载关键资源:使用预加载首屏必需的图片、CSS、JS。
  • 懒加载非首屏资源:图片、视频等资源使用懒加载(loading="lazy"),避免占用首屏加载带宽。
  • 避免使用display: none;(会触发重排),如需隐藏元素,可使用visibility: hidden;(只触发重绘)或opacity: 0;(不触发重排/重绘)。

性能优化实践

  1. 减少重排和重绘

批量 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. 使用合成层优化动画

创建合成层
/* 方法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`;
  }
}

常见问题与解决方案

  1. 白屏问题

问题现象: 页面长时间显示空白

可能原因:

  • 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-visibility CSS 属性
  • 分页加载
/* 使用 content-visibility 优化 */
.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 200px; /* 预估高度 */
}

实战案例分析

案例1:优化首屏渲染时间

场景: 电商首页加载缓慢

问题分析:

  1. CSS 文件过大(200KB)
  2. 关键 CSS 未内联
  3. JavaScript 阻塞渲染
  4. 图片未优化

优化方案:

<!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:优化复杂表单渲染

场景: 包含大量输入框和选择器的表单页面加载慢

问题分析:

  1. 表单元素过多(500+ 个输入框)
  2. 每个输入框都有事件监听器
  3. 样式计算开销大

优化方案:

// ✅ 使用事件委托替代每个元素绑定事件
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 解析 → 阻塞渲染,最终导致页面加载延迟。

(三)解决方案:避免渲染阻塞

  1. JS 标签优化

    1. 外部 JS 放在<body>底部(确保 HTML 先解析完成);

    2. async/defer属性(异步加载 JS,不阻塞 HTML 解析):

      • async:加载完成后立即执行(执行顺序不确定);
      • defer:加载完成后,等待 HTML 解析完成再执行(执行顺序与标签顺序一致);
  2. CSS 优化

    1. 外部 CSS 用<link rel="stylesheet">引入(并行下载,不阻塞 HTML 解析,但阻塞渲染);
    2. 避免在<head>中写大量内联 CSS(增加 CSSOM 构建时间);
  3. 关键 CSS 内联:将首屏渲染必需的 CSS 内联到<head>中,减少外部 CSS 下载延迟。

五、性能优化:基于渲染原理的实战技巧

优化的核心思路是:减少回流、减少重绘、利用合成加速、避免渲染阻塞

(一)减少回流(最关键)

  1. 批量修改 DOM

    1. 避免频繁修改单个 DOM 的样式 / 结构,用DocumentFragment批量操作:
// 优化前(多次回流)
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 布局。

(二)减少重绘

  1. 集中修改样式

    1. 避免频繁修改单个样式属性,用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`的元素脱离渲染树,无重绘。

(三)利用合成加速

  1. 动画用 transform opacity

    1. transform(如translatescale)和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; }

(四)避免渲染阻塞

  1. JS 优化

    1. 首屏非必需的 JS(如统计、广告)用async/defer加载;
    2. 大型 JS 文件用代码分割(Code Splitting),按需加载;
  2. CSS 优化

    1. 外部 CSS 文件放在<head>中(确保样式优先加载,避免 “无样式内容闪烁(FOUC)”);
    2. 非首屏 CSS 用媒体查询media="print"等,不阻塞首屏渲染:
<link rel="stylesheet" href="print.css" media="print"> <!-- 仅打印时加载,不阻塞首屏 -->