React 性能优化完整指南
前置文章:React 组件渲染
通过分析 useCallback、useMemo、React.memo、自定义 Hooks 和 Context 等核心概念,本文档深入探讨了 React 应用中常见的性能问题及其解决方案,提供了系统性的性能优化指导原则和最佳实践。
核心观点:90% 的 useMemo 和 useCallback 可以移除,大多数情况下它们是不必要的,甚至是有害的。
useCallback
useCallback 用于记忆化函数,防止在组件重新渲染时生成新的函数引用,从而避免不必要的子组件重新渲染。然而,在实际开发中,useCallback 经常被过度使用,导致代码复杂化而性能提升有限。
- 完整记忆化链:只有当所有 props 和组件本身都被记忆化时,useCallback 才有意义;
- 避免部分记忆化:一个未记忆化的 prop 会让整个记忆化失效;
- 初始渲染开销:useCallback 在初始渲染时是有害的,会增加额外的开销,只有在重新渲染时才有价值;
推荐使用的场景
1、作为其他 Hooks 的依赖项
const Component = () => {
const [count, setCount] = useState(0);
const fetchData = useCallback(async () => {
console.log("fetchData", count);
}, [count]);
useEffect(() => {
fetchData();
}, [fetchData]); // 只有当 count 改变时才重新获取数据
return <button onClick={() => setCount(count + 1)}>Click me</button>
};
2、传递给被 React.memo 包装的子组件
const MemoizedChild = React.memo(({ onClick, data }: { onClick: () => void, data: { name: string } }) => {
console.log("MemoizedChild 渲染");
return <button onClick={onClick}>{data.name}</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// 数据被 useMemo 记忆化
const data = useMemo(() => ({ name: "test" }), []);
// 函数被 useCallback 记忆化
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
console.log("Parent 渲染");
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* 只有 count 改变时,MemoizedChild 不会重新渲染 */}
<MemoizedChild onClick={handleClick} data={data} />
</div>
);
};
3、复杂逻辑的事件处理函数、自定义 Hook 中返回的函数
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(0);
}, []);
return { count, increment, decrement, reset };
};
const CustomHookExample = () => {
const { count, increment, decrement, reset } = useCounter();
return (
<div>
<span>Count: {count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>重置</button>
</div>
);
};
不应该使用的场景
1、传递给 DOM 元素
const Component = () => {
const [count, setCount] = useState(0);
// DOM 元素不需要记忆化,useCallback 是多余的
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<button onClick={handleClick}>Click me</button>
</div>
);
};
2、传递给未记忆化的组件
// 2. 传递给未记忆化的组件
const Child = ({ onClick }) => {
console.log('Child 渲染'); // 每次父组件渲染都会执行
return <button onClick={onClick}>Click</button>;
};
const Parent = () => {
const [count, setCount] = useState(0);
// useCallback 在这里是无效的,因为 Child 没有记忆化
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* Child 每次都会重新渲染,useCallback 没有意义 */}
<Child onClick={handleClick} />
</div>
);
};
useMemo
useMemo 用于记忆化计算结果,避免在每次渲染时重复进行昂贵的计算。然而,与 useCallback 类似,useMemo 也经常被过度使用。许多开发者会将简单的 JavaScript 操作(如数组排序、对象创建)也包装在 useMemo 中,这不仅没有性能提升,反而增加了初始渲染的开销。
- 初始渲染:useMemo 在初始渲染时是有害的,因为它需要额外的内存和时间来缓存结果。只有当计算成本超过缓存成本时,useMemo 才有价值;
- 重新渲染:重新渲染子组件通常比 JavaScript 计算更昂贵
- 建议:应该记忆化渲染树的重部分,而不是简单的计算。只对真正昂贵的计算使用 useMemo,简单操作反而会增加开销;
经过测试,发现在 6x CPU 减速的情况下,排序 250 个元素的数组只需要 不到 2 毫秒,而渲染这些元素需要超过 20 毫秒(10 倍以上)。
推荐使用的情况
1、记忆化昂贵的计算
const ExpensiveComponent = ({ data }: { data: { value: number, weight: number }[] }) => {
const expensiveValue = useMemo(() => {
// 复杂的数学计算、大数据处理
return data.reduce((acc, item) => {
return acc + Math.pow(item.value, 2) * Math.sqrt(item.weight);
}, 0);
}, [data]);
return <div>计算结果: {expensiveValue}</div>;
};
2、 记忆化渲染树的重部分
interface Item {
id: number;
name: string;
}
const List = ({ items }: { items: Item[] }) => {
const content = useMemo(() => {
console.log("重新渲染列表");
return items.map((item) => (
<ExpensiveChildComponent key={item.id} data={item} />
));
}, [items]);
return <div>{content}</div>;
};
const ExpensiveChildComponent = React.memo(({ data }: { data: Item }) => {
console.log("子组件渲染:", data.id);
return <div>{data.name}</div>;
});
3、作为其他钩子的依赖项
const Component = () => {
const memoizedValue = useMemo(() => ({ id: 1, name: "test" }), []);
useEffect(() => {
// 只有当 memoizedValue 真正改变时才执行
console.log("Value changed");
}, [memoizedValue]);
};
不应该使用的情况
1、简单的 JavaScript 操作
const Component = ({ items }) => {
// 数组排序是简单操作,不需要 useMemo
const sortedItems = useMemo(() => {
console.log("排序数组");
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return (
<div>
{sortedItems.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
// 更好的做法:直接排序
const Component2 = ({ items }) => {
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
return (
<div>
{sortedItems.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
2、对象和数组的简单创建
const Component = ({ name }) => {
const user = useMemo(() => ({ name, id: Date.now() }), [name]);
const styles = useMemo(() => ({ color: "red", fontSize: "16px" }), []);
return <div style={styles}>{user.name}</div>;
};
React.memo
React.memo 是一个高阶组件,用于记忆化组件的渲染结果,防止在相同 props 下不必要的重新渲染。React.memo 通过浅比较 props 来决定是否重新渲染组件。然而,React.memo 只有在特定场景下才有价值:当组件是纯展示组件且 props 变化不频繁时。
React.memo 进行的是浅比较,这意味着它只比较 props 的第一层属性。如果 props 是复杂对象,需要确保这些对象的引用稳定。
- 完整记忆化:只有当每个单个 prop 和组件本身都被记忆化时,才能防止重新渲染;
- 浅比较:React.memo 进行的是浅比较,复杂对象需要确保引用稳定;
- 纯组件:最适合纯展示组件,需要配合稳定的 props 引用,避免复杂状态逻辑;
推荐使用的情况
1、纯展示组件,props 变化不频繁
const UserCard = React.memo(
({
user,
onEdit,
}: {
user: { id: number; name: string; email: string };
onEdit: (id: number) => void;
}) => {
console.log("UserCard 渲染:", user.id);
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
}
);
2、列表项组件
const ListItem = React.memo(
({
item,
onSelect,
}: {
item: { id: number; title: string };
onSelect: (id: number) => void;
}) => {
return <div onClick={() => onSelect(item.id)}>{item.title}</div>;
}
);
3、与 useMemo/useCallback 配合使用
const Parent = () => {
const [count, setCount] = useState(0);
const [users, setUsers] = useState([
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
]);
const handleEdit = useCallback((id) => {
console.log("编辑用户:", id);
}, []);
const usersCards = useMemo(() => {
return users.map((user) => (
<UserCard key={user.id} user={user} onEdit={handleEdit} />
));
}, [users, handleEdit]);
console.log('Parent 渲染');
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{usersCards}
</div>
);
};
不应该使用的情况
1、props 引用不稳定
const Parent = () => {
const [count, setCount] = useState(0);
// 每次渲染都创建新的对象,React.memo 失效
const user = { id: 1, name: "Alice", email: "alice@example.com" };
const handleEdit = (id) => console.log("编辑:", id);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* UserCard 每次都会重新渲染,因为 props 引用不稳定 */}
<UserCard user={user} onEdit={handleEdit} />
</div>
);
};
2、复杂状态组件
const ComplexComponent = React.memo(({ data }: { data: { name: string } }) => {
const [internalState, setInternalState] = useState(0);
const [loading, setLoading] = useState(false);
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: "John" });
}, 1000);
});
};
useEffect(() => {
// 复杂的副作用逻辑
setLoading(true);
fetchData().then(() => setLoading(false));
}, [data]);
return (
<div>
{loading ? "Loading..." : data.name}
<button onClick={() => setInternalState(internalState + 1)}>
{internalState}
</button>
</div>
);
});
自定义 Hooks
自定义 Hooks 是 React 中封装可复用逻辑的强大工具。它们可以帮助我们提取组件逻辑,提高代码的可读性和可维护性。然而,自定义 Hooks 也可能成为性能杀手,特别是当它们无意中将状态提升到更高的组件层级时。这种状态提升会导致不必要的重新渲染,影响整个应用的性能。
自定义 Hooks 中的任何状态改变都会导致宿主组件重新渲染,无论这个状态是否在返回值中暴露。这种状态传播是链式的:如果 Hook A 使用 Hook B,Hook B 的状态改变会导致 Hook A 重新渲染,最终导致宿主组件重新渲染。
- 关键发现:自定义 Hooks 中的状态改变会向上传播,直到到达宿主组件,无论中间是否有记忆化。
- 状态下沉:将状态移动到合适的组件层级,避免在高层级组件中使用包含状态的 Hooks
- 避免内部状态:不要在 hooks 中维护不暴露的内部状态
- 链式传播:hooks 链中的状态改变会向上传播,直到到达宿主组件
❌ 错误示例
状态提升导致整个页面重新渲染
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const [scroll, setScroll] = useState(0); // 内部状态
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
// antd 的 Modal 组件
const Dialog = () => <Modal open={isOpen} onCancel={close} />;
return { isOpen, Dialog, open, close };
};
// 在 Page 组件中使用 自定义 hook
const Page = () => {
const { Dialog, open } = useModal(); // 每次状态改变都会重新渲染整个 Page
return (
<div>
<button onClick={open}>Open</button>
<Dialog />
</div>
);
};
✅ 正确示例
状态下沉到小组件
const SettingsButton = () => {
const { Dialog, open } = useModal(); // 状态被限制在这个小组件中
return (
<>
<button onClick={open}>Open settings</button>
<Dialog />
</>
);
};
const Page = () => {
return (
<div>
<SettingsButton /> {/* 只有这个小组件会重新渲染 */}
</div>
);
};
❌ 链式状态传播问题
// Hook 链中的状态传播
const useScroll = (ref) => {
const [scroll, setScroll] = useState(0);
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleScroll = () => {
setScroll(element?.scrollTop || 0);
};
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
});
return scroll;
};
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
const scroll = useScroll(ref); // 滚动状态改变会导致整个链重新渲染
const Dialog = () => <CustomModal open={isOpen} modalRef={ref} />;
return { Dialog, open: () => setIsOpen(true) };
};
✅ 独立状态隔离
// 将滚动状态隔离到独立组件
const ModalBaseWithScroll = ({ isOpen, onClosed }) => {
const ref = useRef(null);
const scroll = useScroll(ref); // 滚动状态只影响这个组件
console.log("滚动位置:", scroll);
return <CustomModal open={isOpen} onCancel={onClosed} ref={ref} />;
};
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const Dialog = () => (
<ModalBaseWithScroll isOpen={isOpen} onClosed={() => setIsOpen(false)} />
);
return { Dialog, open: () => setIsOpen(true) };
};
Context 性能优化
Context 是 React 中用于跨组件传递数据的机制,它可以帮助我们避免 prop drilling 问题。然而,Context 也可能成为性能杀手,特别是当 Context 中的状态频繁改变时。Context 的工作原理是:当 Context 的值改变时,所有消费该 Context 的组件都会重新渲染。这种重新渲染是不可避免的,但我们可以通过合理的 Context 设计来最小化其影响。
状态分离原则:将状态和 API 分离,使用 Reducer 避免状态依赖,进一步拆分 Context
Context 性能优化的核心是将状态和 API 分离。状态是经常变化的,而 API(函数)通常是稳定的。通过将这两者分离到不同的 Context 中,我们可以确保只有真正需要状态的组件才会重新渲染。
性能优化策略
1. 状态和 API 分离
interface State {
name: string;
discount: number;
}
interface FormAPI {
onSave: () => void;
onDiscountChange: (discount: number) => void;
onNameChange: (name: string) => void;
}
// 分离状态和 API
const FormDataContext = createContext<State>({} as State);
const FormAPIContext = createContext<FormAPI>({} as FormAPI);
const defaultState: State = {
name: "John",
discount: 30,
};
export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<State>(defaultState);
// API 不依赖状态,保持稳定
const api = useMemo(
() => ({
onSave: () => {},
onDiscountChange: (discount: number) => {
setState((prev) => ({ ...prev, discount }));
},
onNameChange: (name: string) => {
setState((prev) => ({ ...prev, name }));
},
}),
[]
); // 空依赖数组
return (
<FormAPIContext.Provider value={api}>
<FormDataContext.Provider value={state}>
{children}
</FormDataContext.Provider>
</FormAPIContext.Provider>
);
};
// 使用不同的 hooks
export const useFormData = () => useContext(FormDataContext);
export const useFormAPI = () => useContext(FormAPIContext);
2. 使用 Reducer 避免状态依赖
type Actions = { type: "updateName"; name: string } | { type: "updateDiscount"; discount: number };
const reducer = (state: State, action: Actions): State => {
switch (action.type) {
case "updateName":
return { ...state, name: action.name };
case "updateDiscount":
return { ...state, discount: action.discount };
}
};
export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, defaultState);
// API 不依赖状态
const api = useMemo(
() => ({
onSave: () => {
// 保存逻辑
},
onDiscountChange: (discount: number) => {
dispatch({ type: "updateDiscount", discount });
},
onNameChange: (name: string) => {
dispatch({ type: "updateName", name });
},
}),
[]
); // 空依赖数组
return (
<FormAPIContext.Provider value={api}>
<FormDataContext.Provider value={state}>
{children}
</FormDataContext.Provider>
</FormAPIContext.Provider>
);
};
3. 进一步拆分状态
// 将状态拆分为多个 Context
const FormNameContext = createContext<State['name']>({} as State['name']);
const FormDiscountContext = createContext<State['discount']>({} as State['discount']);
export const FormProvider3 = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, defaultState);
const api = useMemo(() => ({
onSave: () => {
// 保存逻辑
},
onDiscountChange: (discount: number) => {
dispatch({ type: "updateDiscount", discount });
},
onNameChange: (name: string) => {
dispatch({ type: "updateName", name });
},
}), []);
return (
<FormAPIContext.Provider value={api}>
<FormNameContext.Provider value={state.name}>
<FormDiscountContext.Provider value={state.discount}>
{children}
</FormDiscountContext.Provider>
</FormNameContext.Provider>
</FormAPIContext.Provider>
);
};
// 分别使用不同的状态
export const useFormName = () => useContext(FormNameContext);
export const useFormDiscount = () => useContext(FormDiscountContext);
性能优化最佳原则
1. 先测量,后优化
- 使用 React DevTools Profiler 等工具确认性能问题
- 避免基于猜测进行优化
- 建立性能基准和监控机制
// 使用 React DevTools Profiler 测量性能
import { Profiler, useRef, useEffect } from "react";
const onRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log("Component:", id);
console.log("Phase:", phase);
console.log("Actual Duration:", actualDuration);
console.log("Base Duration:", baseDuration);
console.log("Start Time:", startTime);
console.log("Commit Time:", commitTime);
};
// 自定义性能监控 Hook
const usePerformanceMonitor = (componentName) => {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const endTime = performance.now();
const duration = endTime - startTime.current;
console.log(
`${componentName} 渲染次数: ${
renderCount.current
}, 耗时: ${duration.toFixed(2)}ms`
);
startTime.current = performance.now();
});
return renderCount.current;
};
const MyComponent = () => {
const renderCount = usePerformanceMonitor("MyComponent");
return <div>渲染次数: {renderCount}</div>;
};
const App = () => {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
};
export default App;
2. 避免过早优化
- 只有在确实存在性能问题时才进行优化
- 优先考虑其他优化方法(组件拆分、状态提升等)
- 使用记忆化作为最后手段
3. 完整的记忆化链
- 要么全部记忆化,要么都不记忆化
- 部分记忆化是无效的,会浪费资源
- 确保形成完整的记忆化链
import { useState, useMemo, useCallback } from "react";
import React from "react";
const Parent = () => {
const [count, setCount] = useState(0);
// 数据记忆化
const data = useMemo(() => ({ name: "test" }), []);
// 函数记忆化
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedChild data={data} onClick={handleClick} />
</div>
);
};
const MemoizedChild = React.memo(({ data, onClick }: { data: { name: string }, onClick: () => void }) => {
console.log("MemoizedChild 渲染");
return <button onClick={onClick}>{data.name}</button>;
});
export default Parent;
4. 状态下沉
- 将状态移动到合适的组件层级
- 避免在高层级组件中使用包含状态的 Hooks
- 最小化状态传播范围
import useModal from "antd/es/modal/useModal";
const SettingsButton = () => {
const { Dialog, open } = useModal(); // 状态在小组件层级
return (
<>
<button onClick={open}>Open</button>
<Dialog />
</>
);
};
const App = () => {
return (
<div>
<SettingsButton /> {/* 只有这个小组件会重新渲染 */}
</div>
);
};
export default App;
5. Context 分离原则
- 将状态和 API 分离到不同的 Context
- 使用 Reducer 避免状态依赖
- 进一步拆分 Context 以精确控制重新渲染
完整的表单组件示例
import { Select, Slider } from "antd";
import {
createContext,
useContext,
useReducer,
useMemo,
ReactNode,
} from "react";
import React from "react";
type Country = "USA" | "Canada" | "UK" | "Australia";
type FormState = {
name: string;
country: Country;
discount: number;
};
type Actions =
| { type: "updateName"; name: string }
| { type: "updateCountry"; country: Country }
| { type: "updateDiscount"; discount: number };
type FormAPI = {
onSave: () => void;
onNameChange: (name: string) => void;
onCountryChange: (country: Country) => void;
onDiscountChange: (discount: number) => void;
};
const formReducer = (state: FormState, action: Actions) => {
switch (action.type) {
case "updateName":
return { ...state, name: action.name };
case "updateCountry":
return { ...state, country: action.country };
case "updateDiscount":
return { ...state, discount: action.discount };
}
};
const defaultState: FormState = {
name: "John",
country: "USA",
discount: 10,
};
// 1. 状态和 API 分离的 Context
const FormDataContext = createContext<FormState>({} as FormState);
const FormAPIContext = createContext<FormAPI>({} as FormAPI);
// 2. Provider 实现
export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(formReducer, defaultState);
const api = useMemo(
() => ({
onSave: () => {
// 保存逻辑
},
onNameChange: (name: string) => {
dispatch({ type: "updateName", name });
},
onCountryChange: (country: Country) => {
dispatch({ type: "updateCountry", country });
},
onDiscountChange: (discount: number) => {
dispatch({ type: "updateDiscount", discount });
},
}),
[]
);
return (
<FormAPIContext.Provider value={api}>
<FormDataContext.Provider value={state}>
{children}
</FormDataContext.Provider>
</FormAPIContext.Provider>
);
};
// 3. 自定义 Hooks
export const useFormData = () => useContext(FormDataContext);
export const useFormAPI = () => useContext(FormAPIContext);
// 4. 优化的组件
const NameFormComponent = React.memo(() => {
const { name } = useFormData();
const { onNameChange } = useFormAPI();
return (
<div>
<input value={name} onChange={(e) => onNameChange(e.target.value)} />
</div>
);
});
const CountryFormComponent = React.memo(() => {
const { country } = useFormData();
const { onCountryChange } = useFormAPI();
return (
<Select
value={country}
options={[
{ label: "USA", value: "USA" },
{ label: "Canada", value: "Canada" },
{ label: "UK", value: "UK" },
{ label: "Australia", value: "Australia" },
]}
onChange={onCountryChange}
/>
);
});
const DiscountFormComponent = React.memo(() => {
const { onDiscountChange } = useFormAPI();
return <Slider onChange={onDiscountChange} />;
});
// 5. 主表单组件
const Form = () => {
return (
<FormProvider>
<div>
<NameFormComponent />
<CountryFormComponent />
<DiscountFormComponent />
</div>
</FormProvider>
);
};
export default Form;
性能监控示例
import { useRef, useEffect } from "react";
const usePerformanceMonitor = (componentName: string) => {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const endTime = performance.now();
const duration = endTime - startTime.current;
console.log(
`${componentName} 渲染次数: ${
renderCount.current
}, 耗时: ${duration.toFixed(2)}ms`
);
startTime.current = performance.now();
});
return renderCount.current;
};
// 使用示例
const MyComponent = () => {
const renderCount = usePerformanceMonitor("MyComponent");
return <div>渲染次数: {renderCount}</div>;
};
export default MyComponent;