前端瓶颈
前端性能瓶颈主要可以分为5个阶段。
- 资源加载阶段
- 渲染阶段
- 脚本执行
- 用户交互
- 网络通信
这里主要讨论前端运行时瓶颈,即3、4阶段。这个阶段影响性能最大的元素就是 DOM 操作。而 DOM 操作又为什么会影响性能呢?
这里先简单说一下 DOM 是什么?
DOM 是浏览器维护的复杂结构。
- DOM (Document Object Model)不是纯 Javascript 对象。
- 每次 js 修改 DOM 都要通过浏览器底层引擎同步修改内存结构。
到这里,可以得出一个结论,DOM 操作本身性能瓶颈的原因之一,甚至占比相当大。这里又有一个问题,js 修改 DOM 之后的流程是什么?
有一个经典的面试题,输入一个 url 到他渲染成页面发生了什么?
这个问题简单回答可以分为三个阶段:
- 网络阶段
- 渲染阶段
- js执行阶段
而渲染阶段又会有几个步骤:
| 阶段 | 说明 |
|---|---|
| 解析HTML | HTML Token → DOM Tree(DOM树) |
| 解析CSS | 生成 CSSOM Tree(CSS对象模型树) |
| 合并 DOM + CSSOM | 构建 Render Tree(渲染树) |
| 布局(Layout) | 计算各元素的大小、位置 |
| 绘制(Paint) | 填充像素:颜色、边框、文字、阴影等 |
| 分层和合成(Composite) | 多层内容合成成一张屏幕图像 |
那这个渲染管线,出去第一步,就是 js 操作 DOM 后整个的流程,可以看到这个流程相对复杂,需要执行很多计算,如果要做到高性能,那么这个流程就得尽可能规避。
那什么操作会导致这个流程触发呢?
答案是重绘和重排。
重绘和重排
- 重绘:改变了元素的外观(比如颜色),但没有影响布局。
- 重排(回流):改变了元素的大小、位置、结构,需要重新计算布局。
| 对比点 | 重排 (Reflow) | 重绘 (Repaint) |
|---|---|---|
| 触发原因 | 尺寸、位置、结构变化 | 颜色、背景、阴影等视觉变化 |
| 影响范围 | 当前元素及其所有子元素、兄弟元素 | 只影响当前元素 |
| 计算开销 | 高(布局 + 绘制) | 低(只绘制) |
| 举例 | width, height, top, left, display, font-size | background-color, color, visibility |
| 对性能的影响 | 很大,尤其是复杂页面 | 相对较小 |
重绘很难避免,需要尽量避免重排。
这里有个问题,既然是改变元素属性会导致重排,那么读元素属性会不会导致重排呢?
答案是有可能会。
如何触发重排
强制同步布局。
这里有个概念,叫做强制同步布局。
读取 DOM 元素本身不会直接导致重排,但是如果读取的属性是依赖布局信息的(布局相关属性),就会触发一次重排。说人话就是如果你是先写后读,也就是先修改了 DOM 属性,然后读取时会立刻触发点一次重排,以返回修改后的属性。
element.style.width = '500px';
const height = element.offsetHeight; // 🚨 浏览器此时必须计算最新布局
| 会触发重排的读取属性 | 说明 |
|---|---|
offsetHeight, offsetWidth | 元素尺寸(含边框) |
clientHeight, clientWidth | 元素尺寸(不含滚动条) |
scrollHeight, scrollWidth | 内容区域总高度 |
getBoundingClientRect() | 获取元素位置和尺寸 |
computedStyle | 获取最终计算的样式 |
修改 DOM 树
这个不用说吧
修改 DOM 元素几何样式
这也是肯定的。
优化方案
Fragment
既然 DOM 操作如此耗费性能,且每一次 DOM。操作都会触发一套渲染流程,那么可以将多次 DOM 操作合并成一次,以提高性能。
使用 Fragment 容器提前将所有 DOM 元素渲染并且一次性插入。
requestAnimationFrame
requestAnimationFrame 是浏览器提供的一个API,让你可以在下一帧绘制之前执行一个回调函数。
主要用于高效地执行动画更新、DOM 渲染优化,保证动画流畅、不卡顿,同时不会浪费性能。
浏览器刷新率理想为60帧。 requestAnimationFrame 函数可以告诉浏览器,在下一帧刷新前执行逻辑。
提前缓存节点信息
对于常用且固定的 DOM 元素,可以提前缓存 DOM 属性,以避免触发重绘。
高频事件优化
对于高频事件,例如拖拽,使用防抖和节流进行优化。
最小颗粒度更新
对于主流前端框架,即 Vue 和 React ,在开发时应注意,不要依赖一整个对象,而应该最小粒度依赖,最小粒度更新页面。