react-render-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Render Performance
React 渲染性能
Patterns for minimizing unnecessary React re-renders when consuming external
state. Prefer selector-based subscriptions over —
subscribe only to the slice each component needs.
useState(wholeObject)在使用外部状态时,减少React不必要重渲染的模式。优先使用基于选择器的订阅方式,而非 —— 仅订阅每个组件所需的状态片段。
useState(整个对象)Core idea
核心思想
Storing a full state object in React state (e.g. and
subscribing to every change) forces re-renders on any update. A component
that only needs will still re-render when changes
if both live in the same object.
useState(snapshot)phasequiz.selectedWrongAvoid: subscribe → → read a field in render.
Prefer: subscribe to a selector or slice so the component re-renders only when that value changes. Every library below supports this; use it.
setState(fullObject)Prefer: subscribe to a selector or slice so the component re-renders only when that value changes. Every library below supports this; use it.
Tree position matters. The higher a component is in the tree, the more
expensive a re-render becomes, because React re-renders it and all descendants.
Subscribing to the whole store in is especially bad — every store
change re-renders the entire app. Push subscriptions down to the leaf or
route-level components that actually need the data, or use selectors so
high-level components only re-render when their slice changes.
App.tsx将完整的状态对象存储在React状态中(例如并订阅所有变更)会导致任何更新都触发重渲染。如果某个组件只需要字段,但字段发生变更时(两者在同一个对象中),该组件仍会重渲染。
useState(snapshot)phasequiz.selectedWrong避免: 订阅 → → 在渲染中读取字段。
推荐: 订阅选择器或状态片段,这样组件仅在该值变更时才重渲染。以下所有库都支持此方式,请务必使用。
setState(完整对象)推荐: 订阅选择器或状态片段,这样组件仅在该值变更时才重渲染。以下所有库都支持此方式,请务必使用。
组件树位置很重要。组件在树中的层级越高,重渲染的成本就越高,因为React会重渲染该组件及其所有后代。在中订阅整个存储尤其糟糕——存储的任何变更都会导致整个应用重渲染。将订阅推送到实际需要数据的叶子组件或路由级组件,或者使用选择器,让高层级组件仅在其所需的状态片段变更时才重渲染。
App.tsxLibrary patterns
各库实现模式
XState (actors) — @xstate/react
@xstate/reactXState(角色)—— @xstate/react
@xstate/reactUse so the component re-renders only when the
selected value changes.
useSelector(actor, selector)tsx
// GOOD: stable selector — re-renders only when phase changes
import { useSelector } from "@xstate/react";
import { selectPhase } from "./selectors";
function PhaseIndicator({ actor }) {
const phase = useSelector(actor, selectPhase);
return <Text>{phase}</Text>;
}tsx
// BAD: full snapshot in React state — re-renders on every actor change
const [snapshot, setSnapshot] = useState(null);
useEffect(() => {
const sub = actor.subscribe((snap) => setSnapshot(snap));
return () => sub.unsubscribe();
}, [actor]);
const phase = snapshot?.value?.sessionFlow; // unnecessary re-rendersActor + ref for callbacks: Keep the actor in (so
re-subscribes if the actor is replaced) and in a for synchronous
access in event handlers:
useStateuseSelectoruseReftsx
const [actor, setActor] = useState(() => {
const a = createActor(machine);
a.start();
return a;
});
const actorRef = useRef(actor);
actorRef.current = actor;
function send(event) {
actorRef.current.send(event);
}使用,让组件仅在所选值变更时重渲染。
useSelector(actor, selector)tsx
// 推荐:稳定的选择器 —— 仅当phase变更时重渲染
import { useSelector } from "@xstate/react";
import { selectPhase } from "./selectors";
function PhaseIndicator({ actor }) {
const phase = useSelector(actor, selectPhase);
return <Text>{phase}</Text>;
}tsx
// 不推荐:将完整快照存入React状态 —— 每次actor变更都会重渲染
const [snapshot, setSnapshot] = useState(null);
useEffect(() => {
const sub = actor.subscribe((snap) => setSnapshot(snap));
return () => sub.unsubscribe();
}, [actor]);
const phase = snapshot?.value?.sessionFlow; // 不必要的重渲染角色 + Ref用于回调: 将actor保存在中(这样当actor被替换时,会重新订阅),同时用保存,以便在事件处理程序中同步访问:
useStateuseSelectoruseReftsx
const [actor, setActor] = useState(() => {
const a = createActor(machine);
a.start();
return a;
});
const actorRef = useRef(actor);
actorRef.current = actor;
function send(event) {
actorRef.current.send(event);
}@xstate/store — @xstate/store-react
@xstate/store-react@xstate/store —— @xstate/store-react
@xstate/store-reactUse to subscribe to a slice of store context.
Re-renders only when the selected value changes (strict equality by default;
optional custom ).
useSelector(store, selector)comparetsx
// GOOD: select one field — re-renders only when count changes
import { createStore, useSelector } from "@xstate/store-react";
const store = createStore({
context: { count: 0, name: "" },
on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) },
});
function CountDisplay() {
const count = useSelector(store, (state) => state.context.count);
return <span>{count}</span>;
}tsx
// BAD: selecting whole context — re-renders on any context change
const context = useSelector(store, (state) => state.context);
return <span>{context.count}</span>;Custom comparison when the selector returns an object:
tsx
const user = useSelector(
store,
(state) => state.context.user,
(prev, next) => prev.id === next.id
);使用订阅存储上下文的片段。仅当所选值变更时才重渲染(默认使用严格相等比较;可自定义函数)。
useSelector(store, selector)comparetsx
// 推荐:选择单个字段 —— 仅当count变更时重渲染
import { createStore, useSelector } from "@xstate/store-react";
const store = createStore({
context: { count: 0, name: "" },
on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) },
});
function CountDisplay() {
const count = useSelector(store, (state) => state.context.count);
return <span>{count}</span>;
}tsx
// 不推荐:选择整个上下文 —— 上下文任何变更都会触发重渲染
const context = useSelector(store, (state) => state.context);
return <span>{context.count}</span>;当选择器返回对象时,可使用自定义比较:
tsx
const user = useSelector(
store,
(state) => state.context.user,
(prev, next) => prev.id === next.id
);Zustand
Zustand
Use the store with a selector as the first argument. The component
re-renders only when the selected value changes (referential equality).
tsx
// GOOD: selector — re-renders only when count changes
const count = useStore((state) => state.count);
// GOOD: primitive or stable ref — minimal re-renders
const phase = useStore((state) => state.session.phase);tsx
// BAD: no selector — re-renders on every store change
const state = useStore();
return <span>{state.count}</span>;tsx
// BAD: selecting a new object every time — re-renders every time
const { count, name } = useStore((state) => ({ count: state.count, name: state.name }));
// Use two selectors or useShallow insteadUse a module-level selector so the function reference is stable (see
Selector rules below). For multiple fields, use or pick
primitives:
useShallowtsx
import { useShallow } from "zustand/react/shallow";
const { count, name } = useStore(useShallow((state) => ({ count: state.count, name: state.name })));将选择器作为第一个参数传入useStore。组件仅在所选值变更时重渲染(基于引用相等性)。
tsx
// 推荐:选择器 —— 仅当count变更时重渲染
const count = useStore((state) => state.count);
// 推荐:原始值或稳定引用 —— 最小化重渲染
const phase = useStore((state) => state.session.phase);tsx
// 不推荐:无选择器 —— 每次存储变更都会重渲染
const state = useStore();
return <span>{state.count}</span>;tsx
// 不推荐:每次都返回新对象 —— 每次都会重渲染
const { count, name } = useStore((state) => ({ count: state.count, name: state.name }));
// 改用两个选择器或useShallow使用模块级选择器以确保函数引用稳定(见下文选择器规则)。如需多个字段,使用或单独选择原始值:
useShallowtsx
import { useShallow } from "zustand/react/shallow";
const { count, name } = useStore(useShallow((state) => ({ count: state.count, name: state.name })));Redux — react-redux
react-reduxRedux —— react-redux
react-reduxUse and select the smallest slice needed. Redux uses
referential equality; selecting a new object every time forces re-renders.
useSelector(selector)tsx
// GOOD: select a primitive or stable reference
const phase = useSelector((state) => state.session.phase);
const count = useSelector((state) => state.counter);tsx
// BAD: selecting whole slice — new object ref when any part of session updates
const session = useSelector((state) => state.session);
return <span>{session.phase}</span>;For object slices use or a memoized selector:
shallowEqualtsx
import { shallowEqual, useSelector } from "react-redux";
const { phase, step } = useSelector(
(state) => ({ phase: state.session.phase, step: state.session.step }),
shallowEqual
);使用并选择最小的所需状态片段。Redux使用引用相等性;如果每次选择都返回新对象,会强制触发重渲染。
useSelector(selector)tsx
// 推荐:选择原始值或稳定引用
const phase = useSelector((state) => state.session.phase);
const count = useSelector((state) => state.counter);tsx
// 不推荐:选择整个片段 —— session任何部分更新都会生成新对象引用
const session = useSelector((state) => state.session);
return <span>{session.phase}</span>;对于对象片段,使用或记忆化选择器:
shallowEqualtsx
import { shallowEqual, useSelector } from "react-redux";
const { phase, step } = useSelector(
(state) => ({ phase: state.session.phase, step: state.session.step }),
shallowEqual
);Nanostores — @nanostores/react
@nanostores/reactNanostores —— @nanostores/react
@nanostores/reactNanostores doesn’t take a selector in the hook; shape your stores so each
consumer subscribes to a small store. Use computed stores to derive
slices, or split state into multiple atoms.
tsx
// GOOD: one atom per logical slice, or computed for a derived slice
import { atom, computed } from "nanostores";
import { useStore } from "@nanostores/react";
const $session = atom({ phase: "idle", step: 0 });
const $phase = computed($session, (s) => s.phase);
function PhaseIndicator() {
const phase = useStore($phase); // re-renders only when phase changes
return <Text>{phase}</Text>;
}tsx
// BAD: one big store, useStore on the whole thing — re-renders on any change
const $app = atom({ session: {...}, quiz: {...}, ui: {...} });
function PhaseIndicator() {
const app = useStore($app);
return <Text>{app.session.phase}</Text>;
}Use map or atoms for granular updates and computed for derived
values; then each component s only the store it needs.
useStoreNanostores的Hook不支持选择器;设计你的存储结构,让每个消费者订阅一个小型存储。使用computed存储派生状态片段,或将状态拆分为多个atom。
tsx
// 推荐:每个逻辑片段对应一个atom,或用computed派生片段
import { atom, computed } from "nanostores";
import { useStore } from "@nanostores/react";
const $session = atom({ phase: "idle", step: 0 });
const $phase = computed($session, (s) => s.phase);
function PhaseIndicator() {
const phase = useStore($phase); // 仅当phase变更时重渲染
return <Text>{phase}</Text>;
}tsx
// 不推荐:单个大型存储,订阅整个存储 —— 任何变更都会重渲染
const $app = atom({ session: {...}, quiz: {...}, ui: {...} });
function PhaseIndicator() {
const app = useStore($app);
return <Text>{app.session.phase}</Text>;
}使用map或atom实现细粒度更新,使用computed处理派生值;这样每个组件仅它所需的存储。
useStoreReact context
React Context
Context re-renders all consumers when the value reference changes. Prefer
splitting by update frequency or exposing a subscribable store and
selecting in the consumer.
tsx
// GOOD: split by update frequency
<FrequentContext.Provider value={frequentData}>
<RareContext.Provider value={rareData}>
{children}
</RareContext.Provider>
</RareContext.Provider>tsx
// GOOD: store in context, select in consumer (e.g. Zustand store, XState actor)
function useSessionPhase() {
const store = useContext(StoreContext);
return useSelector(store, (s) => s.phase);
}tsx
// BAD: one context with everything — any change re-renders all consumers
<AppContext.Provider value={{ user, session, theme, settings, ... }}>当Context的值引用变更时,所有消费者都会重渲染。优先按更新频率拆分Context,或暴露可订阅的存储并在消费者中选择所需状态。
tsx
// 推荐:按更新频率拆分
<FrequentContext.Provider value={frequentData}>
<RareContext.Provider value={rareData}>
{children}
</RareContext.Provider>
</FrequentContext.Provider>tsx
// 推荐:将存储放入Context,在消费者中选择状态(例如Zustand存储、XState角色)
function useSessionPhase() {
const store = useContext(StoreContext);
return useSelector(store, (s) => s.phase);
}tsx
// 不推荐:单个Context包含所有内容 —— 任何变更都会导致所有消费者重渲染
<AppContext.Provider value={{ user, session, theme, settings, ... }}>useSyncExternalStore (custom stores)
useSyncExternalStore(自定义存储)
For stores that aren’t one of the above, use React’s and
subscribe to a slice in so the component only re-renders
when that slice changes.
useSyncExternalStoregetSnapshottsx
// GOOD: getSnapshot returns only the slice this component needs
const phase = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().session.phase,
() => store.getSnapshot().session.phase
);tsx
// BAD: getSnapshot returns full state — re-renders on every store change
const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
return <span>{state.session.phase}</span>;对于上述之外的存储,使用React的,并在中订阅状态片段,这样组件仅在该片段变更时重渲染。
useSyncExternalStoregetSnapshottsx
// 推荐:getSnapshot仅返回该组件所需的片段
const phase = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().session.phase,
() => store.getSnapshot().session.phase
);tsx
// 不推荐:getSnapshot返回完整状态 —— 每次存储变更都会重渲染
const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
return <span>{state.session.phase}</span>;Selector rules
选择器规则
- Keep selectors at module level — not inline in the component. Inline arrow functions create new references each render and can defeat equality checks.
tsx
// GOOD
const selectPhase = (snap) => snap.value?.sessionFlow;
function MyComponent({ actor }) {
const phase = useSelector(actor, selectPhase);
}
// BAD — new function ref every render
function MyComponent({ actor }) {
const phase = useSelector(actor, (snap) => snap.value?.sessionFlow);
}-
Return primitives or stable references. If the selector returns a new object/array every time, the component will re-render on every update. Prefer primitives or use a custom comparison when you must return an object.
-
Don’t put expensive derivation in selectors. Heavy work belongs inin the component, not in the selector (selectors run often).
useMemo
- 将选择器放在模块级别 —— 不要在组件内联。内联箭头函数每次渲染都会创建新的引用,会破坏相等性检查。
tsx
// 推荐
const selectPhase = (snap) => snap.value?.sessionFlow;
function MyComponent({ actor }) {
const phase = useSelector(actor, selectPhase);
}
// 不推荐 —— 每次渲染都会创建新函数引用
function MyComponent({ actor }) {
const phase = useSelector(actor, (snap) => snap.value?.sessionFlow);
}-
返回原始值或稳定引用。如果选择器每次都返回新的对象/数组,组件会在每次更新时都重渲染。优先返回原始值;如果必须返回对象,使用自定义比较函数。
-
不要在选择器中执行昂贵的推导。繁重的计算应放在组件的中,而非选择器(选择器会频繁执行)。
useMemo
Anti-patterns
反模式
| Anti-pattern | Why it's bad | Fix |
|---|---|---|
| Every store/actor change re-renders | Use selector / slice (useSelector, selector arg, computed store) |
| No selector / whole store in hook | Same as above | Pass selector to useStore/useSelector; or use computed/small stores |
| Inline selector function | New reference each render | Module-level selector |
| Selector returns new object every time | Always re-renders | Return primitive or use shallowEqual/custom compare |
| Mega-context with everything | Any update re-renders all consumers | Split context or put a store in context and select in consumer |
| 反模式 | 危害 | 修复方案 |
|---|---|---|
在订阅中使用 | 每次存储/角色变更都会触发重渲染 | 使用选择器/状态片段(useSelector、选择器参数、computed存储) |
| Hook中无选择器/订阅整个存储 | 同上 | 为useStore/useSelector传入选择器;或使用computed/小型存储 |
| 内联选择器函数 | 每次渲染创建新引用 | 使用模块级选择器 |
| 选择器每次返回新对象 | 总是触发重渲染 | 返回原始值或使用shallowEqual/自定义比较 |
| 包含所有内容的巨型Context | 任何更新都会导致所有消费者重渲染 | 拆分Context,或在Context中放入存储并在消费者中选择状态 |
When to use selectors
何时使用选择器
Use a selector / slice when:
- The component needs 1–2 fields from a larger state
- Different fields update at different rates (e.g. phase rarely, quiz state often)
- Several components each need different parts of the same store
- The component is high in the tree (e.g. , layout, root route) — re-renders there cascade down the whole tree, so avoid subscribing to the whole store at that level
App.tsx
A single subscription is OK when:
- The component needs most or all of the state
- Updates are rare (e.g. user profile)
- There’s only one consumer or it’s a leaf with no children
Rule of thumb: If a component re-renders more often than its visible output
changes, add a selector (or a computed/small store). Use React DevTools
Profiler to confirm.
当以下情况时,使用选择器/状态片段:
- 组件只需要大型状态中的1-2个字段
- 不同字段的更新频率不同(例如phase很少更新,quiz状态频繁更新)
- 多个组件分别需要同一存储的不同部分
- 组件在组件树中层级较高(例如、布局组件、根路由)—— 此处的重渲染会向下级联整个树,因此避免在此层级订阅整个存储
App.tsx
单个订阅是可接受的情况:
- 组件需要大部分或全部状态
- 更新频率很低(例如用户资料)
- 只有一个消费者,或者是没有子组件的叶子组件
经验法则: 如果组件的重渲染频率高于其可见输出的变更频率,请添加选择器(或computed/小型存储)。可使用React DevTools Profiler确认。