渲染优化
React 函数式组件的渲染优化
React 的渲染机制是 函数式组件每次渲染都会重新执行函数。重复渲染可能是正常现象,也可能是性能问题。
在 React 函数式组件 (Functional Component) 中,渲染优化的核心目标是:避免不必要的重新渲染 和 减少渲染时的性能开销。
减少不必要的重新渲染
1️⃣ 使用 React.memo
对函数式组件做浅层的 props 比较,只有当 props 改变时才重新渲染
Child 组件不会因为 App 的 state 变化而重复渲染,除非 value 改变。
import React from "react";
const Child = React.memo(({ value }: { value: number }) => {
console.log("child render");
return <div>{value}</div>;
});
export default function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<Child value={1} />
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
2️⃣ 使用 useMemo
对计算开销大的值(如过滤、排序、大对象生成等)进行缓存,避免每次渲染都重复计算。
const filtered = useMemo(() => {
return list.filter((item) => item.active);
}, [list]);
3️⃣ 使用 useCallback
父组件每次渲染都会生成新的函数引用,传给子组件时可能导致子组件重复渲染。用 useCallback
保证函数在依赖不变时不会变。
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
// 子组件接收这个 handleClick 就不会因为父组件渲染而变化
4️⃣ 避免不必要的 context 更新
Context 更新会触发所有消费该 context 的组件更新。
优化方案:
- 拆分 context(粒度更细)
- 用 memo 包裹 context value
- 或用第三方库(如 zustand、jotai、redux)
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
<ThemeContext.Provider value={value}>...</ThemeContext.Provider>;
减少渲染时的性能开销
1️⃣ 合理使用 Key
key 的选择影响 React 的 diff 算法。用稳定、唯一的标识,避免因为 key 改变导致整行组件被销毁重建。
2️⃣ 避免在 JSX 里直接写内联函数 / 对象
- 每次渲染都会创建新引用,容易导致子组件重新渲染。
- 建议提前定义或用 useCallback/useMemo。
❌
<Child style={{ color: "red" }} onClick={() => doSomething()} />
✅
const style = useMemo(() => ({ color: "red" }), []);
const handleClick = useCallback(() => doSomething(), []);
<Child style={style} onClick={handleClick} />
加载优化
1️⃣ React.lazy 和 Suspense
使用 React.lazy + Suspense 实现按需加载。
const LazyComponent = React.lazy(() => import("./LazyComponent"));
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>;
2️⃣ 图片懒加载
3️⃣ 请求缓存
用 React Query、SWR 缓存和预取数据,减少重复请求。
高级优化
1️⃣ 并发特性
使用 React 18 的 useTransition、useDeferredValue 优化交互体验。
const [isPending, startTransition] = useTransition();
startTransition(() => {
setCount(count + 1);
});
2️⃣ PWA & 缓存
使用 Service Worker 缓存资源,提高离线体验。
其他常见问题
useMemo 和 useCallback 用的越多越好吗
不,useMemo 和 useCallback 并不是越多越好,过度使用反而可能带来负面影响。
原因如下:
- 内存开销增加。每次组件渲染时,React 会存储 memo 缓存。过多的 useMemo 或 useCallback 会占用额外内存
- 计算和比较开销。useMemo 和 useCallback 的依赖比较也有成本(浅比较依赖数组)。如果计算本身很轻量,使用 useMemo 可能比直接计算更慢。
使用建议:
- 只有在性能敏感时使用
- 当函数或计算值传递给深层子组件,且这些子组件通过 React.memo 或 PureComponent 优化时
- 当计算量大时,使用 useMemo 避免每次渲染重复计算
- 不要为了“缓存”而缓存所有东西
- 简单值、简单函数通常直接使用即可,不需要包裹
- 依赖数组要准确
- 避免漏写依赖,否则缓存值可能过期或出错
如何监控页面卡顿
1️⃣ Performance API
用于测量页面加载、资源、渲染等性能指标
// 获取页面从开始加载到DOMContentLoaded的时间
console.log(performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart);
// 或者 PerformanceObserver 监控 Long Tasks
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log("Long Task Detected:", entry);
});
});
observer.observe({ entryTypes: ["longtask"] });
2️⃣ requestAnimationFrame + 时间差
监控帧率(FPS),如果帧率持续低于 50fps,说明页面可能卡顿。
let last = performance.now();
function monitorFrame() {
const now = performance.now();
const delta = now - last;
if (delta > 50) {
console.warn("Frame drop detected:", delta, "ms");
}
last = now;
requestAnimationFrame(monitorFrame);
}
requestAnimationFrame(monitorFrame);
3️⃣ 输入延迟监控
使用 Event Timing API 或第三方库记录用户交互的延迟。
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log("First Input Delay:", entry.processingStart - entry.startTime, "ms");
});
}).observe({ type: "first-input", buffered: true });