渲染优化
问题
浏览器是如何渲染页面的?什么是重排和重绘?如何减少渲染性能问题?
答案
渲染优化的核心是减少浏览器的渲染工作量,避免不必要的重排(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';
});
});