渲染性能
渲染性能关注的是浏览器从 HTML/CSS/JS 到屏幕像素的整个过程。理解这条流水线,才能解释"为什么动画会卡""为什么滚动不流畅""为什么操作 DOM 慢"。这是中高级前端面试的高频考点,特别是 React/Vue 这种组件化框架普及后,理解底层渲染机制对优化尤为关键。
浏览器渲染流水线
从接收 HTML 到绘制像素,浏 览器大致经历以下阶段:
HTML → DOM → Style → Layout → Paint → Composite → 像素
树构建 样式计算 布局计算 绘制 合成
每个阶段的职责:
| 阶段 | 输入 | 输出 | 说明 |
|---|---|---|---|
| DOM | HTML 字符串 | DOM 树 | 解析标签构建节点树 |
| Style | DOM + CSS | ComputedStyle | 计算每个元素的最终样式 |
| Layout | DOM + ComputedStyle | 布局树 | 计算几何信息(位置、大小) |
| Paint | 布局树 | 绘制指令列表 | 生成绘制顺序、填充文字图片等 |
| Composite | 绘制指令 | 图层 | 将各图层合成最终画面 |
关键:JS、Style、Layout、Paint、Composite 这五个阶段中,只有 Composite 一定在合成线程上执行,其他都可能阻塞主线程。所以优化的核心思路之一就是让更多工作跳过前三步,直接走合成。
重排(Reflow)与重绘(Repaint)
什么是重排
当修改影响几何信息的属性时,浏览器需要重新计算布局,这个过程称为重排(layout reflow)。
会触发重排的属性(部分):
- 盒模型:
width、height、margin、padding、border - 位置:
top、left、right、bottom、float、position - 文本:
font-size、text-align、line-height、white-space - 显隐:
display - 内容:
innerHTML、appendChild、removeChild
什么是重绘
当修改视觉属性但不影响几何信息时,只需重新绘制,称为重绘。
会触发重绘的属性(部分):
- 颜色:
color、background-color、border-color - 阴影:
box-shadow、text-shadow - 可见性:
visibility、outline
重排与重绘的关系
重排必然导致重绘,重绘不一定导致重排。重排的开销远大于重绘。
重排 → 重新 Layout → 重新 Paint → 重新 Composite
重绘 → 直接 Paint → 重新 Composite
合成 → 直接 Composite
只触发合成的属性
修改以下属性不会触发重排和重绘,只触发合成,性能最好:
transform:translate、scale、rotateopacityfilterwill-change(提示而非直接触发)
/* 推荐:用 transform 做位移,不走 Layout/Paint */
.move {
transition: transform 0.3s;
transform: translateX(100px);
}
/* 避免:用 left/top 触发重排 */
.bad {
transition: left 0.3s;
left: 100px;
}
布局抖动(Layout Thrashing)
布局抖动是强制同步布局(Forced Synchronous Layout)的连续触发,是常见的性能反模式。
触发原理
当你在修改 DOM 后立刻读取几何属性,浏览器为了给你最新的值,必须立即执行一次同步布局计算:
// 反模式:在循环中读写交替,每次读都会触发同步布局
const boxes = document.querySelectorAll('.box');
for (let i = 0; i < boxes.length; i++) {
boxes[i].style.width = box.offsetWidth + 10 + 'px'; // 写
console.log(box.offsetHeight); // 读 → 强制同步布局
}
规避方法:读写分离
把所有"读"操作集中起来,先读后写:
const boxes = document.querySelectorAll('.box');
// 先读取所有需要的几何信息
const widths = Array.from(boxes, el => el.offsetWidth);
// 再批量写入,避免读写交替
boxes.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px';
});
使用 FastDOM 库或类似的 API 设计,本质就是把读写阶段分离。
哪些 API 会触发同步布局
常见的"读"操作会触发同步布局:
offsetTop/offsetLeft/offsetWidth/offsetHeightscrollTop/scrollLeft/scrollWidth/scrollHeightclientTop/clientWidth/clientHeightgetComputedStyle()getBoundingClientRect()window.scrollTo()之后立刻读取scrollY
注意:使用
ResizeObserver监听尺寸变化,比直接轮询offsetWidth性能更好。