前端性能优化:如何避免重排(Reflow)和重绘(Repaint)
前端性能优化:如何避免重排(Reflow)和重绘(Repaint)
在前端性能优化中,重排和重绘是两个非常重要的概念。
页面滚动卡顿、动画掉帧、交互延迟,很多时候并不是 JavaScript 计算本身太慢,而是代码频繁触发了浏览器的样式计算、布局计算和像素绘制。
本文将系统讲解:
- 浏览器渲染页面的基本流程;
- 什么是重排和重绘;
- 哪些操作容易触发重排和重绘;
- 什么是强制同步布局和布局抖动;
- 如何通过批量读写、缓存布局、
requestAnimationFrame、transform、虚拟列表等方式优化; - Vue 和 React 项目中的常见优化方法;
- 如何使用 Chrome DevTools 定位布局和绘制问题。
一、浏览器是如何渲染页面的
浏览器将 HTML、CSS 和 JavaScript 转换成最终页面,大致会经过以下流程:
HTML
↓
DOM Tree
↓
CSS
↓
CSSOM Tree
↓
Render Tree
↓
Style
↓
Layout
↓
Paint
↓
Composite
其中几个关键阶段分别是:
1. Style
浏览器根据 CSS 选择器、继承关系和层叠规则,计算每个元素最终生效的样式。
例如:
.container .item.active {
width: 200px;
color: #1677ff;
}
浏览器需要确认:
- 哪些元素匹配该选择器;
- 哪些属性被覆盖;
- 哪些属性被继承;
- 每个元素最终使用什么样式。
2. Layout
Layout 也称为 Reflow,即重排。
浏览器会计算每个元素的:
- 宽度;
- 高度;
- 位置;
- 内边距;
- 外边距;
- 相对父元素的位置;
- 对其他元素的影响。
3. Paint
Paint 即绘制。
浏览器会把元素绘制成像素,包括:
- 文字;
- 背景;
- 边框;
- 阴影;
- 图片;
- 渐变;
- 圆角。
4. Composite
Composite 即图层合成。
浏览器会将不同图层合成为最终画面,再交给屏幕显示。
现代浏览器中的动画优化,很多时候就是尽量让变化只发生在 Composite 阶段,避免重新执行 Layout 和 Paint。
二、什么是重排
重排,也称为:
- Reflow;
- Layout。
当元素的几何信息发生变化时,浏览器需要重新计算页面布局。
例如:
const box = document.querySelector(".box");
box.style.width = "300px";
如果 .box 的宽度发生变化,浏览器可能需要重新计算:
.box自身尺寸;.box子元素的位置;.box后续兄弟元素的位置;- 父元素高度;
- 页面滚动区域。
这就是重排。
三、为什么重排成本较高
重排通常不是只影响当前元素。
例如:
<div class="layout">
<header class="header">Header</header>
<main class="content">Content</main>
<footer class="footer">Footer</footer>
</div>
如果修改 Header 高度:
const header = document.querySelector(".header");
header.style.height = "200px";
可能导致:
Header 高度变化
→ Content 向下移动
→ Footer 向下移动
→ Layout 总高度变化
→ 页面滚动高度变化
→ 相关区域重新绘制
DOM 数量越多、布局层级越复杂,重排的成本通常越高。
四、什么是重绘
重绘,也称为:
- Repaint;
- Paint。
当元素的外观发生变化,但尺寸和位置没有变化时,浏览器通常只需要重新绘制元素。
例如:
const box = document.querySelector(".box");
box.style.backgroundColor = "#1677ff";
背景色变化不会改变元素的宽高和位置,因此一般只会触发重绘。
常见可能触发重绘的属性包括:
color
background-color
border-color
box-shadow
outline
visibility
五、重排和重绘的关系
通常情况下:
重排会引起重绘,但重绘不一定引起重排。
例如修改宽度:
修改 width
→ Layout
→ Paint
→ Composite
修改背景色:
修改 background-color
→ Paint
→ Composite
修改 transform:
修改 transform
→ Composite
因此,一般可以把性能成本理解为:
Layout > Paint > Composite
但实际成本仍然取决于:
- DOM 数量;
- 重排影响范围;
- 绘制面积;
- CSS 效果复杂度;
- 浏览器实现;
- 设备性能。
六、哪些操作容易触发重排
1. 修改宽度和高度
element.style.width = "300px";
element.style.height = "200px";
常见属性:
width
height
min-width
max-width
min-height
max-height
2. 修改内边距和外边距
element.style.padding = "20px";
element.style.marginTop = "16px";
常见属性:
padding
margin
3. 修改元素位置
element.style.top = "100px";
element.style.left = "50px";
常见属性:
top
right
bottom
left
4. 修改字体相关属性
element.style.fontSize = "20px";
element.style.lineHeight = "32px";
常见属性:
font-size
font-family
font-weight
line-height
letter-spacing
字体变化可能影响文字宽度、换行和元素高度。
5. 添加或删除 DOM 节点
container.appendChild(newElement);
container.removeChild(oldElement);
DOM 结构变化后,浏览器通常需要重新计算布局。
6. 修改 display
element.style.display = "none";
display: none 会让元素退出文档流,因此通常会触发重排。
重新显示:
element.style.display = "block";
元素重新参与布局,同样可能触发重排。
7. 修改 class
element.classList.add("active");
是否触发重排,取决于 class 修改了哪些属性。
例如:
.active {
width: 300px;
}
会影响布局。
而:
.active {
color: red;
}
通常只影响绘制。
8. 改变窗口尺寸
用户调整浏览器窗口大小时,响应式布局需要重新计算。
window.addEventListener("resize", () => {
updateLayout();
});
resize 是高频事件,回调中如果包含复杂布局计算,容易造成卡顿。
七、哪些读取操作可能触发强制同步布局
很多开发者认为只有“写入样式”才会触发重排。
实际上,某些读取布局信息的操作,也可能迫使浏览器立即执行 Layout。
常见 API:
element.offsetWidth
element.offsetHeight
element.offsetTop
element.offsetLeft
element.clientWidth
element.clientHeight
element.scrollWidth
element.scrollHeight
element.scrollTop
element.getBoundingClientRect()
window.getComputedStyle(element)
例如:
element.style.width = "300px";
const width = element.offsetWidth;
第一行修改样式后,浏览器原本可能准备稍后统一计算布局。
但第二行要求立即返回最新宽度,所以浏览器必须马上执行 Layout。
这种现象称为:
Forced Synchronous Layout
即强制同步布局。
八、什么是布局抖动
布局抖动,也称为:
Layout Thrashing
典型特征是不断交叉执行:
写 DOM
→ 读布局
→ 写 DOM
→ 读布局
错误示例:
const items = document.querySelectorAll(".item");
items.forEach((item) => {
item.style.width = "200px";
const height = item.offsetHeight;
item.style.height = `${height + 10}px`;
});
每次循环都可能发生:
修改 width
→ 读取 offsetHeight
→ 强制执行 Layout
→ 修改 height
→ 下一轮再次执行 Layout
如果列表中有几百或几千个元素,性能会明显下降。
九、优化原则:批量读取,批量写入
避免布局抖动最重要的原则是:
先统一读取布局信息,再统一写入样式。
错误写法:
const items = document.querySelectorAll(".item");
items.forEach((item) => {
const width = item.offsetWidth;
item.style.width = `${width + 20}px`;
});
推荐写法:
const items = [
...document.querySelectorAll(".item"),
];
const widths = items.map((item) => {
return item.offsetWidth;
});
items.forEach((item, index) => {
item.style.width = `${widths[index] + 20}px`;
});
执行过程变为:
批量读取
→ 批量写入
而不是:
读取
→ 写入
→ 读取
→ 写入
十、缓存布局信息
如果一个布局值不会频繁变化,就不应该每次事件触发时都重新读取。
不推荐:
window.addEventListener("scroll", () => {
const target = document.querySelector(".target");
const targetTop = target.offsetTop;
if (window.scrollY > targetTop) {
showHeader();
}
});
问题:
- 每次滚动都查询 DOM;
- 每次滚动都读取布局;
- 高频事件中重复计算。
推荐:
const target = document.querySelector(".target");
let targetTop = target.offsetTop;
function updateTargetTop() {
targetTop = target.offsetTop;
}
window.addEventListener("resize", updateTargetTop);
window.addEventListener("scroll", () => {
if (window.scrollY > targetTop) {
showHeader();
}
});
如果页面中有动态内容导致位置变化,可以在必要时重新计算,而不是每一帧都计算。
十一、批量修改样式
1. 使用 class 切换状态
不推荐:
element.style.width = "200px";
element.style.height = "100px";
element.style.marginTop = "20px";
element.style.backgroundColor = "#1677ff";
element.style.color = "#fff";
推荐:
element.classList.add("active");
.active {
width: 200px;
height: 100px;
margin-top: 20px;
color: #fff;
background-color: #1677ff;
}
优点:
- 样式集中管理;
- 可维护性更高;
- 减少多次内联样式赋值;
- 方便浏览器统一处理样式变化。
2. 使用 cssText
也可以一次性写入多个样式:
element.style.cssText = `
width: 200px;
height: 100px;
margin-top: 20px;
color: #fff;
background-color: #1677ff;
`;
需要注意:
设置
cssText可能覆盖元素原有的内联样式。
因此,项目中通常更推荐使用 class。
十二、批量插入 DOM
1. 逐个插入
不推荐:
const list = document.querySelector(".list");
for (let i = 0; i < 1000; i++) {
const item = document.createElement("div");
item.textContent = `Item ${i}`;
list.appendChild(item);
}
逐个插入会反复修改 DOM 树。
2. 使用 DocumentFragment
推荐:
const list = document.querySelector(".list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement("div");
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
list.appendChild(fragment);
先在内存中完成节点构建,最后一次性插入页面。
3. 使用字符串统一生成
const html = Array.from(
{
length: 1000,
},
(_, index) => {
return `
<div class="item">
Item ${index}
</div>
`;
}
).join("");
document.querySelector(".list").innerHTML = html;
适用于静态内容。
但要注意:
- 不要直接插入未经处理的用户输入;
- 防止 XSS;
- 替换
innerHTML会销毁旧节点和旧事件绑定。
十三、动画优先使用 transform 和 opacity
在动画中频繁修改以下属性,通常会触发布局:
top
left
width
height
margin
padding
错误示例:
let left = 0;
function animate() {
left += 1;
element.style.left = `${left}px`;
requestAnimationFrame(animate);
}
animate();
如果每一帧修改 left,浏览器可能持续执行布局计算。
推荐:
let x = 0;
function animate() {
x += 1;
element.style.transform = `translateX(${x}px)`;
requestAnimationFrame(animate);
}
animate();
transform 通常不会改变文档流布局,更适合高频动画。
推荐的动画属性
transform
opacity
示例:
.card {
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.card:hover {
transform: translateY(-8px) scale(1.02);
opacity: 0.95;
}
不推荐频繁动画的属性
width
height
top
left
margin
padding
font-size
例如:
.panel {
width: 200px;
transition: width 0.3s;
}
.panel.open {
width: 400px;
}
这种动画可能持续触发布局。
可以根据视觉需求考虑使用位移、缩放或遮罩替代。
十四、使用 requestAnimationFrame 合并视觉更新
requestAnimationFrame 会在浏览器下一次绘制前执行回调。
适合:
- 滚动更新;
- 拖拽;
- 动画;
- 高频视觉变化。
示例:
let ticking = false;
window.addEventListener(
"scroll",
() => {
if (ticking) {
return;
}
ticking = true;
requestAnimationFrame(() => {
updateScrollProgress();
ticking = false;
});
},
{
passive: true,
}
);
即使一帧内滚动事件触发多次,页面也只会更新一次。
十五、滚动进度条优化示例
HTML:
<div class="scroll-progress"></div>
CSS:
.scroll-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
transform: scaleX(0);
transform-origin: left center;
background-color: #1677ff;
z-index: 9999;
}
JavaScript:
const progress = document.querySelector(
".scroll-progress"
);
let ticking = false;
function updateProgress() {
const scrollTop =
document.documentElement.scrollTop;
const maxScrollTop =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const ratio =
maxScrollTop > 0
? scrollTop / maxScrollTop
: 0;
progress.style.transform = `scaleX(${ratio})`;
}
window.addEventListener(
"scroll",
() => {
if (ticking) {
return;
}
ticking = true;
requestAnimationFrame(() => {
updateProgress();
ticking = false;
});
},
{
passive: true,
}
);
这里使用:
transform: scaleX()
而不是反复修改:
width
可以减少布局计算。
十六、display、visibility 和 opacity 的区别
display: none
.hidden {
display: none;
}
特点:
- 不占据布局空间;
- 不响应事件;
- 会改变页面布局;
- 通常触发重排。
visibility: hidden
.hidden {
visibility: hidden;
}
特点:
- 仍然占据布局空间;
- 元素不可见;
- 不响应鼠标事件;
- 通常不会改变布局结构。
opacity: 0
.hidden {
opacity: 0;
}
特点:
- 仍然占据布局空间;
- 默认仍可能响应事件;
- 通常只涉及绘制或合成。
如果透明元素不应点击,可以补充:
.hidden {
opacity: 0;
pointer-events: none;
}
对比如下:
| 方式 | 占据布局空间 | 可见 | 默认可交互 | 通常是否影响布局 |
|---|---|---|---|---|
display: none |
否 | 否 | 否 | 是 |
visibility: hidden |
是 | 否 | 否 | 否 |
opacity: 0 |
是 | 否 | 是 | 否 |
十七、正确使用 will-change
will-change 可以提前告诉浏览器,某个属性即将变化。
.card {
will-change: transform;
}
浏览器可能提前为元素创建独立合成层。
适用于:
- 高频 transform 动画;
- 拖拽元素;
- 即将开始的复杂动画。
不要这样写:
* {
will-change: transform;
}
也不要给大量元素长期设置 will-change。
因为可能导致:
- 创建过多图层;
- 占用更多显存;
- 图层管理成本增加;
- 合成性能下降。
可以在动画前添加,动画结束后移除:
element.style.willChange = "transform";
element.addEventListener(
"transitionend",
() => {
element.style.willChange = "auto";
},
{
once: true,
}
);
十八、使用 content-visibility 优化长页面
对于内容很长的页面,可以使用:
.section {
content-visibility: auto;
contain-intrinsic-size: 600px;
}
content-visibility: auto 可以让浏览器跳过视口外内容的布局和绘制。
适合:
- 长文章;
- 长设置页面;
- 多模块后台页面;
- 复杂内容区块。
示例:
<section class="section">
大量复杂内容
</section>
.section {
content-visibility: auto;
contain-intrinsic-size: 600px;
}
contain-intrinsic-size 用于提供预估尺寸,减少滚动条变化和页面跳动。
使用前需要结合浏览器兼容性评估。
十九、使用 CSS Containment 限制影响范围
CSS 的 contain 属性可以限制元素内部变化对外部布局的影响。
.card {
contain: layout paint;
}
常见值:
contain: layout;
contain: paint;
contain: size;
contain: style;
contain: content;
contain: strict;
适合:
- 仪表盘卡片;
- 独立组件;
- 列表项;
- 互不影响的内容区块。
例如:
.dashboard-card {
contain: layout paint;
}
浏览器可以更明确地知道:
- 卡片内部布局不会影响外部;
- 绘制区域不会超出卡片边界。
需要注意:
contain: size等设置可能影响元素尺寸计算,不能盲目使用。
二十、长列表使用虚拟滚动
如果页面中有几千或几万条数据,即使单次重排次数不多,布局计算仍然会很慢。
普通列表:
<div
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</div>
如果 list 有 10000 条数据,就会创建大量 DOM 节点。
虚拟列表只渲染当前可见区域:
总数据:10000 条
实际 DOM:20~50 条
适用场景:
- 大型表格;
- 日志列表;
- 消息列表;
- 搜索结果;
- 后台数据列表。
Vue 常用方案:
vue-virtual-scroller
Element Plus Virtualized Table
React 常用方案:
react-window
react-virtualized
二十一、图片和媒体元素提前设置尺寸
图片加载完成后,如果页面没有预留尺寸,内容可能突然向下移动。
不推荐:
<img
src="/banner.jpg"
alt="Banner"
/>
推荐:
<img
src="/banner.jpg"
width="1200"
height="600"
alt="Banner"
/>
或者使用 aspect-ratio:
.image-wrapper {
aspect-ratio: 2 / 1;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
这样可以:
- 提前预留图片空间;
- 减少页面布局变化;
- 改善 CLS 指标。
二十二、字体加载导致的布局变化
Web Font 加载前后,字体宽度和高度可能不同。
例如:
body {
font-family: "AppFont", sans-serif;
}
字体加载完成后,可能引起:
- 文字重新换行;
- 行高变化;
- 按钮宽度变化;
- 页面整体偏移。
优化方式:
- 选择尺寸接近的备用字体;
- 使用
font-display; - 预加载关键字体;
- 减少字体文件和字重数量。
示例:
@font-face {
font-family: "AppFont";
src: url("/fonts/app.woff2") format("woff2");
font-display: swap;
}
预加载:
<link
rel="preload"
href="/fonts/app.woff2"
as="font"
type="font/woff2"
crossorigin
/>
二十三、表格布局优化
复杂表格容易频繁计算列宽。
可以使用:
table {
width: 100%;
table-layout: fixed;
}
table-layout: fixed 可以减少浏览器根据全部单元格内容反复计算列宽的成本。
大型表格还应考虑:
- 固定列宽;
- 虚拟滚动;
- 减少复杂单元格;
- 避免每个单元格单独绑定事件;
- 分页加载;
- 批量更新数据。
二十四、使用事件委托减少监听器数量
大量子元素不需要分别绑定事件。
不推荐:
const items = document.querySelectorAll(".item");
items.forEach((item) => {
item.addEventListener("click", handleClick);
});
推荐使用事件委托:
const list = document.querySelector(".list");
list.addEventListener("click", (event) => {
const item = event.target.closest(".item");
if (!item || !list.contains(item)) {
return;
}
handleClick(item);
});
优点:
- 减少事件监听器数量;
- 动态新增节点无需重新绑定;
- 降低内存占用;
- 简化列表维护。
事件委托不直接消除重排,但能减少大型 DOM 结构中的额外开销。
二十五、使用浏览器观察器 API
很多场景不需要在滚动或缩放事件中持续读取布局。
IntersectionObserver
适用于:
- 图片懒加载;
- 元素曝光;
- 无限滚动;
- 判断元素是否进入视口。
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
loadContent(entry.target);
observer.unobserve(entry.target);
});
}
);
observer.observe(
document.querySelector(".target")
);
ResizeObserver
适用于监听某个元素尺寸变化:
const observer = new ResizeObserver(
(entries) => {
for (const entry of entries) {
console.log(entry.contentRect);
}
}
);
observer.observe(
document.querySelector(".container")
);
相比监听全局 resize,ResizeObserver 通常更精确。
二十六、Vue 3 中避免不必要的布局更新
1. 避免模板中执行复杂函数
不推荐:
<template>
<div>
{{ calculateLayoutResult() }}
</div>
</template>
组件每次重新渲染,都可能再次执行该函数。
推荐使用 computed:
<script setup>
import { computed } from "vue";
const layoutResult = computed(() => {
return calculateLayoutResult();
});
</script>
<template>
<div>
{{ layoutResult }}
</div>
</template>
2. 避免深度监听大型对象
不推荐:
watch(
largeConfig,
() => {
updateLayout();
},
{
deep: true,
}
);
大型对象中任何深层属性变化,都可能触发回调。
推荐只监听必要字段:
watch(
() => [
largeConfig.width,
largeConfig.height,
],
() => {
updateLayout();
}
);
3. 保持 key 稳定
推荐:
<div
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</div>
不推荐在频繁增删和排序的列表中使用索引:
<div
v-for="(item, index) in list"
:key="index"
>
{{ item.name }}
</div>
不稳定的 key 可能导致 Vue 错误复用或重新创建更多 DOM。
4. 正确使用 v-if 和 v-show
v-if:
<Panel v-if="visible" />
特点:
- 条件为 false 时不创建 DOM;
- 切换时会创建或销毁节点;
- 适合切换频率较低的场景。
v-show:
<Panel v-show="visible" />
本质是切换:
display: none;
特点:
- DOM 一直存在;
- 切换成本较低;
- 适合频繁显示和隐藏;
- 仍会影响布局。
5. 大量静态内容使用 v-once
<div v-once>
这部分内容不会再变化
</div>
v-once 可以避免后续重复更新静态内容。
二十七、React 中减少布局开销
1. 避免不必要的组件重新渲染
const Item = React.memo(function Item({
item,
}) {
return <div>{item.name}</div>;
});
当 props 没有变化时,React.memo 可以避免子组件重复渲染。
2. 保持 props 引用稳定
不推荐:
<Item
style={{
width: 200,
}}
/>
每次渲染都会创建新的对象。
可以使用 class:
<Item className="item" />
或者:
const itemStyle = useMemo(
() => ({
width: 200,
}),
[]
);
3. 谨慎使用 useLayoutEffect
useLayoutEffect 会在浏览器绘制前同步执行。
useLayoutEffect(() => {
const rect =
elementRef.current.getBoundingClientRect();
setWidth(rect.width);
}, []);
如果在其中执行复杂计算或同步更新状态,可能阻塞浏览器绘制。
能使用 useEffect 的场景,不要强行使用 useLayoutEffect。
4. 大型列表使用虚拟化
React 中可以使用:
react-window
react-virtualized
避免一次性渲染几千个 DOM 节点。
二十八、避免复杂 CSS 绘制
以下效果可能增加重绘成本:
box-shadow
filter: blur()
backdrop-filter
border-radius
gradient
mix-blend-mode
例如:
.card {
backdrop-filter: blur(20px);
box-shadow:
0 20px 40px
rgba(0, 0, 0, 0.25);
}
如果页面中有大量此类卡片,在滚动或动画时可能出现明显掉帧。
优化方式:
- 减少模糊半径;
- 减少阴影层数;
- 避免大面积
backdrop-filter; - 减小绘制区域;
- 避免同时动画阴影和模糊;
- 对重复复杂背景使用图片资源。
二十九、完整优化案例
下面是一段容易出现性能问题的代码:
const cards = document.querySelectorAll(
".card"
);
window.addEventListener("scroll", () => {
cards.forEach((card) => {
const rect =
card.getBoundingClientRect();
if (rect.top < window.innerHeight) {
card.style.top =
`${window.scrollY * 0.1}px`;
card.style.opacity = "1";
} else {
card.style.opacity = "0";
}
});
});
问题包括:
- 滚动事件高频触发;
- 每次循环都调用
getBoundingClientRect(); - 读取布局后立即写入样式;
- 修改
top可能触发布局; - 所有卡片都参与计算;
- 没有使用
requestAnimationFrame; - 没有设置
passive: true。
优化后:
const cards = [
...document.querySelectorAll(".card"),
];
const visibleCards = new Set();
let ticking = false;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
visibleCards.add(entry.target);
} else {
visibleCards.delete(entry.target);
}
});
}
);
cards.forEach((card) => {
observer.observe(card);
});
function updateVisibleCards() {
const offset = window.scrollY * 0.1;
visibleCards.forEach((card) => {
card.style.transform =
`translate3d(0, ${offset}px, 0)`;
card.style.opacity = "1";
});
}
window.addEventListener(
"scroll",
() => {
if (ticking) {
return;
}
ticking = true;
requestAnimationFrame(() => {
updateVisibleCards();
ticking = false;
});
},
{
passive: true,
}
);
优化点:
- 使用
IntersectionObserver获取可见元素; - 只更新可见卡片;
- 使用
requestAnimationFrame; - 使用
transform替代top; - 使用
passive: true; - 避免滚动时读取所有元素布局。
三十、如何使用 Chrome DevTools 排查问题
1. Performance 面板
操作步骤:
- 打开 Chrome DevTools;
- 切换到
Performance; - 点击录制;
- 执行滚动、动画或点击;
- 停止录制;
- 查看 Main Thread。
重点关注:
Recalculate Style
Layout
Paint
Composite Layers
如果 Layout 占用很高,说明布局计算较重。
如果 Paint 区域很大,说明页面存在大面积重绘。
2. 查看 Forced Reflow
如果 Performance 面板出现:
Forced reflow
通常说明代码在修改样式后,立即读取了布局信息。
例如:
element.style.width = "300px";
console.log(element.offsetWidth);
3. Paint Flashing
打开:
DevTools
→ More tools
→ Rendering
→ Paint flashing
开启后,发生重绘的区域会被高亮。
如果滚动时整个页面不断闪烁,说明绘制范围可能过大。
4. Layout Shift Regions
在 Rendering 面板中启用:
Layout Shift Regions
用于观察页面中的布局偏移,适合排查 CLS 问题。
5. Performance Monitor
打开:
DevTools
→ More tools
→ Performance monitor
可以观察:
- CPU 使用率;
- JS Heap;
- DOM Nodes;
- Layouts / sec;
- Style recalculations / sec。
如果每秒 Layout 次数很高,需要检查高频 DOM 读写。
三十一、使用 Performance API 测量代码耗时
可以使用 performance.now():
const startTime = performance.now();
updateLayout();
const endTime = performance.now();
console.log(
`执行耗时:${endTime - startTime}ms`
);
也可以使用标记:
performance.mark("layout-start");
updateLayout();
performance.mark("layout-end");
performance.measure(
"layout-task",
"layout-start",
"layout-end"
);
const entries =
performance.getEntriesByName(
"layout-task"
);
console.log(entries);
需要注意:
JavaScript 函数耗时不完全等于浏览器 Layout 和 Paint 的耗时,最终仍需结合 DevTools Performance 分析。
三十二、常见误区
1. 所有 DOM 更新都会立即重排
不一定。
现代浏览器会尽量合并多次样式修改。
但是,如果代码要求立即读取最新布局,浏览器会被迫同步执行 Layout。
例如:
element.style.width = "300px";
const width = element.offsetWidth;
2. transform 一定不会重绘
不完全正确。
transform 通常不会触发布局,但仍可能涉及:
- 图层合成;
- 纹理上传;
- 某些情况下重新绘制。
它的主要优势是尽量避开 Layout 阶段。
3. 重绘一定很便宜
不一定。
如果绘制区域很大,或者存在:
- 大面积模糊;
- 多层阴影;
- 半透明叠加;
- 渐变;
- 大型背景图;
Paint 的成本仍可能很高。
4. will-change 越多越快
错误。
过多的 will-change 会创建大量图层,增加显存和合成成本。
5. 框架会自动解决所有性能问题
错误。
Vue 和 React 可以优化 DOM 更新过程,但如果最终修改的是:
width
height
top
left
浏览器仍然需要执行布局。
框架无法消除浏览器渲染阶段的基础成本。
三十三、面试常见问题
1. 重排和重绘有什么区别
重排是重新计算元素的位置和尺寸。
重绘是元素几何信息不变,但外观发生变化后重新绘制像素。
重排通常会引起重绘,重绘不一定会引起重排。
2. 什么是强制同步布局
当 JavaScript 修改样式后,又立即读取布局信息时,浏览器为了返回准确值,会被迫马上执行 Layout。
例如:
element.style.width = "300px";
console.log(element.offsetWidth);
3. 什么是布局抖动
布局抖动是指反复交叉执行 DOM 读取和 DOM 写入:
写入样式
→ 读取布局
→ 写入样式
→ 读取布局
这会导致浏览器频繁执行同步布局。
4. 哪些属性适合做动画
优先使用:
transform
opacity
它们通常不会触发布局,并且更容易由合成层处理。
5. 如何减少重排
常见方法:
- 批量读取 DOM;
- 批量写入 DOM;
- 缓存布局信息;
- 使用 class 批量修改样式;
- 使用
DocumentFragment; - 动画使用
transform和opacity; - 使用
requestAnimationFrame; - 减少 DOM 数量;
- 使用虚拟列表;
- 使用
IntersectionObserver和ResizeObserver。
三十四、性能优化检查清单
排查重排和重绘时,可以依次检查:
- 是否在循环中读取
offsetWidth或offsetHeight; - 是否在滚动事件中频繁调用
getBoundingClientRect(); - 是否交叉执行 DOM 读取和写入;
- 是否频繁修改
width、height、top、left; - 动画是否可以改用
transform; - 是否重复查询同一个 DOM;
- 是否可以缓存布局信息;
- 是否可以使用
requestAnimationFrame; - 是否可以使用
IntersectionObserver; - 是否可以使用
ResizeObserver; - 大列表是否需要虚拟滚动;
- 图片是否设置了宽高或
aspect-ratio; - 字体加载是否导致布局偏移;
- 是否存在大面积阴影、模糊和透明叠加;
- 是否错误使用大量
will-change; - DevTools 中 Layout 和 Paint 是否占用过高。
三十五、总结
避免重排和重绘的核心,不是完全消除它们,而是:
减少触发次数、缩小影响范围、避免强制同步布局,并让视觉更新尽量停留在合成阶段。
重点记住以下原则。
1. 批量读取,批量写入
先读 DOM
再写 DOM
不要反复执行:
写
→ 读
→ 写
→ 读
2. 动画优先使用 transform 和 opacity
优先:
transform
opacity
尽量避免高频修改:
top
left
width
height
margin
padding
3. 高频更新使用 requestAnimationFrame
requestAnimationFrame(() => {
updateUI();
});
4. 减少 DOM 数量
大型页面可以考虑:
- 虚拟滚动;
content-visibility;- CSS containment;
- 按需渲染;
- 分页加载。
5. 使用浏览器观察器 API
优先考虑:
IntersectionObserver
ResizeObserver
而不是在滚动和缩放事件中反复读取布局。
6. 使用 DevTools 验证优化结果
重点查看:
Recalculate Style
Layout
Paint
Composite Layers
Forced Reflow
最后用一句话概括:
减少布局计算,避免读写交叉;动画使用 transform,视觉更新跟随浏览器帧节奏。
更多推荐



所有评论(0)