跳到主要内容

渲染优化

问题

浏览器是如何渲染页面的?什么是重排和重绘?如何减少渲染性能问题?

答案

渲染优化的核心是减少浏览器的渲染工作量,避免不必要的重排(Layout)和重绘(Paint),利用 GPU 加速实现流畅的 60fps 体验。


浏览器渲染流程

阶段说明触发条件
Parse解析 HTML/CSS文档加载
Style计算样式CSS 变化
Layout(重排)计算几何信息尺寸/位置变化
Paint(重绘)绘制像素颜色/背景变化
Composite图层合成transform/opacity

重排(Reflow)

重排是浏览器重新计算元素几何属性(位置、尺寸)的过程,成本最高

触发重排的属性

// 几何属性
const layoutProperties = [
'width', 'height',
'padding', 'margin', 'border',
'top', 'left', 'right', 'bottom',
'position', 'display', 'float',
'font-size', 'line-height',
'text-align', 'vertical-align',
'overflow', 'white-space'
];

// 触发重排的操作
const layoutOperations = [
'offsetWidth/Height', // 读取
'clientWidth/Height',
'scrollWidth/Height',
'getComputedStyle()',
'getBoundingClientRect()'
];

强制同步布局

// ❌ 强制同步布局(Layout Thrashing)
function bad() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
// 读取导致强制布局
const width = el.offsetWidth;
// 写入触发重排
el.style.width = width + 10 + 'px';
});
}

// ✅ 读写分离
function good() {
const elements = document.querySelectorAll('.item');

// 批量读取
const widths = Array.from(elements).map(el => el.offsetWidth);

// 批量写入
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px';
});
}

重绘(Repaint)

重绘是重新绘制元素外观的过程,不涉及布局计算,成本低于重排。

只触发重绘的属性

const paintOnlyProperties = [
'color',
'background',
'border-color',
'border-style',
'border-radius',
'box-shadow',
'outline',
'visibility'
];

合成(Composite)

只触发合成的属性是最高效的,直接由 GPU 处理。

触发合成的属性

// 只触发合成,性能最佳
const compositeProperties = [
'transform',
'opacity',
'filter',
'will-change'
];

GPU 加速

创建合成层

/* 使用 transform 创建合成层 */
.accelerated {
transform: translateZ(0);
/* 或 */
transform: translate3d(0, 0, 0);
/* 或 */
will-change: transform;
}

/* 动画使用 transform 而非 left/top */
.animation {
/* ❌ 触发重排 */
/* left: 100px; */

/* ✅ 只触发合成 */
transform: translateX(100px);
}

will-change 优化

/* 提前告知浏览器将要变化的属性 */
.will-animate {
will-change: transform, opacity;
}

/* 动画结束后移除 */
.will-animate.done {
will-change: auto;
}
// JavaScript 动态管理 will-change
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});

element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
注意

过度使用 will-change 会消耗大量内存,应在需要时添加,不需要时移除。


优化技巧

批量 DOM 操作

// ❌ 多次操作 DOM
function bad() {
for (let i = 0; i < 1000; i++) {
document.body.appendChild(createItem(i));
}
}

// ✅ 使用 DocumentFragment
function good() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
fragment.appendChild(createItem(i));
}
document.body.appendChild(fragment);
}

// ✅ 使用 innerHTML(简单场景)
function better() {
const html = Array.from({ length: 1000 })
.map((_, i) => `<div class="item">${i}</div>`)
.join('');
container.innerHTML = html;
}

离线 DOM 操作

// 方式1:display: none
element.style.display = 'none';
// 多次修改...
element.style.display = 'block';

// 方式2:cloneNode
const clone = element.cloneNode(true);
// 修改 clone...
element.parentNode.replaceChild(clone, element);

// 方式3:DocumentFragment
const fragment = document.createDocumentFragment();
// 操作 fragment...
container.appendChild(fragment);

避免频繁读取布局属性

// ❌ 循环中读取布局属性
function bad() {
const width = element.offsetWidth; // 每次都触发重排
// ...
}

// ✅ 缓存布局属性
function good() {
const width = element.offsetWidth; // 只读一次
for (let i = 0; i < 100; i++) {
// 使用缓存的 width
}
}

使用 CSS 类批量修改

// ❌ 多次修改样式
element.style.width = '100px';
element.style.height = '100px';
element.style.background = 'red';

// ✅ 使用 CSS 类
element.classList.add('active');
.active {
width: 100px;
height: 100px;
background: red;
}

动画优化

使用 transform 和 opacity

/* ❌ 触发重排的动画 */
@keyframes move-bad {
from { left: 0; }
to { left: 100px; }
}

/* ✅ 只触发合成的动画 */
@keyframes move-good {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}

/* ❌ 触发重绘的淡入 */
@keyframes fade-bad {
from { visibility: hidden; }
to { visibility: visible; }
}

/* ✅ 只触发合成的淡入 */
@keyframes fade-good {
from { opacity: 0; }
to { opacity: 1; }
}

requestAnimationFrame

// ❌ setTimeout 动画
function animateBad() {
element.style.left = parseInt(element.style.left) + 1 + 'px';
setTimeout(animateBad, 16);
}

// ✅ requestAnimationFrame 动画
function animateGood() {
element.style.transform = `translateX(${x++}px)`;
if (x < 100) {
requestAnimationFrame(animateGood);
}
}
requestAnimationFrame(animateGood);

CSS 动画 vs JS 动画

// 简单动画用 CSS
// 复杂控制用 JS + Web Animations API

// Web Animations API
element.animate([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0 }
], {
duration: 1000,
easing: 'ease-in-out',
fill: 'forwards'
});

React 渲染优化

// 1. React.memo 避免不必要的重渲染
const MemoizedComponent = React.memo(({ data }) => {
return <div>{data}</div>;
});

// 2. useMemo 缓存计算结果
function Component({ items }) {
const expensiveResult = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);

return <div>{expensiveResult}</div>;
}

// 3. useCallback 缓存函数
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);

return <Child onClick={handleClick} />;
}

// 4. 列表 key
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

Vue 渲染优化

<script setup lang="ts">
import { computed, shallowRef } from 'vue';

// 1. computed 缓存
const filteredList = computed(() => {
return list.value.filter(item => item.active);
});

// 2. shallowRef 浅响应
const largeData = shallowRef<LargeData[]>([]);

// 3. v-once 只渲染一次
// <div v-once>{{ staticContent }}</div>

// 4. v-memo 缓存子树
// <div v-memo="[item.id]">{{ item.name }}</div>
</script>

<template>
<!-- 5. key 优化 -->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>

<!-- 6. 组件懒加载 -->
<Suspense>
<AsyncComponent />
</Suspense>
</template>

性能检测

Chrome DevTools

// Performance 面板
// 1. 录制性能
// 2. 查看 Layout/Paint 时间
// 3. 分析长任务

// Rendering 面板
// 1. Paint flashing - 查看重绘区域
// 2. Layout Shift Regions - 查看布局偏移
// 3. FPS meter - 查看帧率

// Layers 面板
// 查看合成层

Performance API

// 测量渲染时间
performance.mark('render-start');

// 渲染操作...
renderComponent();

performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');

const measure = performance.getEntriesByName('render')[0];
console.log(`渲染耗时: ${measure.duration}ms`);

常见面试问题

Q1: 什么是重排和重绘?如何避免?

答案

概念说明触发条件性能影响
重排重新计算布局尺寸、位置变化
重绘重新绘制外观颜色、背景变化
合成GPU 图层合成transform、opacity

避免方法

  • 批量修改 DOM
  • 使用 CSS 类
  • 避免读取触发重排的属性
  • 使用 transform/opacity 做动画
  • 使用 will-change 提示

Q2: 如何优化动画性能?

答案

/* 1. 使用 transform 而非 left/top */
.animate {
transform: translateX(100px);
}

/* 2. 使用 opacity 而非 visibility */
.fade {
opacity: 0;
}

/* 3. 开启 GPU 加速 */
.gpu {
will-change: transform;
/* 或 transform: translateZ(0); */
}
// 4. 使用 requestAnimationFrame
function animate() {
// 更新动画
requestAnimationFrame(animate);
}

// 5. 使用 Web Animations API
element.animate([...keyframes], options);

Q3: 什么是合成层?如何创建?

答案

合成层是独立的渲染层,由 GPU 直接处理,变化时不影响其他层。

/* 创建合成层的方法 */
.composite-layer {
/* 3D 变换 */
transform: translateZ(0);
transform: translate3d(0, 0, 0);

/* will-change */
will-change: transform;
will-change: opacity;

/* 其他 */
opacity: 0.99; /* 小于 1 */
filter: blur(0);
}

Q4: 如何检测渲染性能问题?

答案

// 1. Chrome DevTools
// - Performance 录制
// - Rendering > Paint flashing
// - Layers 面板查看合成层

// 2. Performance API
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 50) {
console.warn('Long task:', entry);
}
});
});
observer.observe({ entryTypes: ['longtask'] });

// 3. requestAnimationFrame 检测帧率
let lastTime = performance.now();
let frames = 0;

function checkFPS() {
frames++;
const now = performance.now();
if (now - lastTime >= 1000) {
console.log('FPS:', frames);
frames = 0;
lastTime = now;
}
requestAnimationFrame(checkFPS);
}

Q5: 强制同步布局是什么?如何避免?

答案

强制同步布局(Forced Synchronous Layout)是在 JavaScript 中交替读写样式导致的性能问题。

// ❌ 强制同步布局
elements.forEach(el => {
const width = el.offsetWidth; // 读取 → 强制布局
el.style.width = width + 10; // 写入 → 标记需要重排
});

// ✅ 读写分离
const widths = elements.map(el => el.offsetWidth); // 批量读
elements.forEach((el, i) => {
el.style.width = widths[i] + 10; // 批量写
});

// ✅ 使用 requestAnimationFrame 分离
requestAnimationFrame(() => {
// 批量写入,下一帧统一处理
elements.forEach(el => {
el.style.width = '100px';
});
});

相关链接