重排、重绘、合成:浏览器渲染的“三兄弟”,你惹不起也躲不过

你给一个元素悄悄改了宽度,结果整个页面都抖了一下?你加了个动画,电脑风扇开始狂转?今天我们来认识浏览器渲染里的"三兄弟"------重排、重绘、合成。弄懂它们,你就能写出流畅60帧的页面,告别卡顿。

前言

想象一下,你家客厅要重新装修。你只是换了个抱枕(重绘),很轻松。但如果你要把墙拆了(重排),那得搬家具、砸墙、重新粉刷,累得半死。如果只是把电视画面换个图层(合成),连工人都不要,遥控器一按就行。

浏览器的渲染也是这个道理。理解这三种操作的成本,就能写出性能飞起的页面。

一、先复习:渲染流水线

之前我们讲过,浏览器把HTML/CSS变成屏幕上的像素,要经过:DOM树 + CSSOM树 → 渲染树 → 布局(计算位置大小)→ 绘制(填充像素)→ 合成(合并图层)。

其中:

  • 重排(Reflow):重新计算布局(位置、大小)。成本最高。
  • 重绘(Repaint):重新绘制像素(颜色、背景、阴影等)。成本中等。
  • 合成(Composite):重新合并图层。成本极低(走GPU)。

二、重排:动到筋骨,全员遭殃

什么操作会触发重排?

  • 改变元素的几何属性widthheightmarginpaddingbordertopleft......
  • 改变DOM结构:增删元素、改变内容(文字变了导致高度变化)。
  • 读取某些布局属性offsetTopscrollTopclientWidthgetComputedStyle()。因为浏览器需要返回最新值,不得不强制重排。
  • 改变窗口大小(resize事件)。
  • 激活伪类 (如:hover导致样式变化影响布局)。

重排的代价:浏览器要重新计算整个或部分渲染树,然后重新布局、绘制、合成。就像你拆了一面墙,整个房子都得重新量尺寸。

三、重绘:只换皮肤,不动骨架

什么操作会触发重绘但不触发重排?

  • 改变颜色colorbackground-colorborder-colorbox-shadow等。
  • 改变可见性visibility(但display: none会触发重排)。
  • 改变背景图outline等。

重绘的代价:不需要重新布局,但还是要重新绘制像素,比重排轻,但也不是免费。

四、合成:GPU加速的"超车道"

合成是成本最低的环节,因为它不涉及布局和绘制,只把已有的图层合并。能触发合成的属性有:

  • transform(平移、旋转、缩放)
  • opacity
  • filter

当你用transform: translateZ(0)will-change: transform时,浏览器会把这个元素提升到单独的合成层 ,后续动画只由GPU处理,完全不触发重排和重绘。这就是为什么动画推荐用transform而不是left

css 复制代码
/* 差:触发重排 */
.box {
  transition: left 0.3s;
  left: 0;
}
.box:hover {
  left: 100px;
}

/* 好:只触发合成 */
.box {
  transition: transform 0.3s;
  transform: translateX(0);
}
.box:hover {
  transform: translateX(100px);
}

五、如何减少重排和重绘?

1. 批量修改样式

不要挨个改属性,用class一次改完:

js 复制代码
// 差
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 好
element.classList.add('new-size');

2. 让元素脱离文档流再操作

比如要插入多个li,可以先隐藏(display: none),改完再显示,只触发两次重排。

js 复制代码
const ul = document.getElementById('list');
ul.style.display = 'none';
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  ul.appendChild(li);
}
ul.style.display = 'block';

3. 使用文档片段(DocumentFragment)

js 复制代码
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li);
}
ul.appendChild(fragment); // 只触发一次重排

4. 读写分离

不要交替读取和修改布局属性,否则会触发多次重排。

js 复制代码
// 差
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = boxes[i].offsetWidth + 'px'; // 读后立即写
}

// 好:先读后写
const widths = [];
for (let i = 0; i < boxes.length; i++) {
  widths.push(boxes[i].offsetWidth);
}
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = widths[i] + 'px';
}

5. 使用transformopacity做动画

永远不要用lefttopwidthmargin做动画,改用transform

6. 固定元素位置

position: fixedabsolute的元素,其重排影响范围较小(只在自己层内)。

7. 避免使用table布局

一个小改动可能触发整个table的重排。

六、实战:一个性能优化的例子

假设你要做一个跟随鼠标移动的小光点(类似鼠标特效)。错误做法:每帧改变top/left,触发重排。正确做法:用transform

js 复制代码
// 差:每移动1px就重排一次
dot.style.left = x + 'px';
dot.style.top = y + 'px';

// 好:只触发合成
dot.style.transform = `translate(${x}px, ${y}px)`;

七、怎么分析页面重排/重绘?

Chrome DevTools → Performance 面板,录制一段操作,查看"Layout Shift"、"Paint"等标记。红色紫色区域越少越好。

八、总结:三兄弟的"饭量"

  • 重排:吃满汉全席,最贵。动几何、DOM结构。
  • 重绘:吃快餐,中等。动颜色、背景。
  • 合成:喝矿泉水,几乎免费。动transform、opacity。

优化口诀:能用transform别用left,能用class别改style,读写分离,批量操作。

如果你觉得今天的"三兄弟"够形象,点个赞让更多人看到。明天我们将聊聊JavaScript引擎与内存管理,看看V8是怎么给代码"打扫卫生"的。我们明天见!

相关推荐
NickJiangDev2 小时前
Elpis-Core 技术解析:从零构建一个基于 Koa 的企业级 Node.js 框架内核
前端
我要让全世界知道我很低调2 小时前
来聊聊 Codex 高效编程的正确姿势
前端·程序员
NickJiangDev2 小时前
Elpis Webpack 工程化实战:Vue 多页应用的构建体系搭建
前端
米饭同学i2 小时前
GitLab CI/CD + Vue 前端 完整方案
前端
yuki_uix2 小时前
遇到前端题目,我现在会先问自己这四个问题
前端·面试
Wect2 小时前
JS 手撕:对象创建、继承全解析
前端·javascript·面试
PeterMap2 小时前
Vue.js全面解析:从入门到上手,前端新手的首选框架
前端·vue.js
3秒一个大2 小时前
深入理解 JS 中的栈与堆:从内存模型到数据结构,再谈内存泄漏
前端·javascript·数据结构
Mr_Xuhhh2 小时前
深入Java多线程进阶:从锁策略到并发工具全解析
前端·数据库·python