react-render-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Render Performance

React 渲染性能

Patterns for minimizing unnecessary React re-renders when consuming external state. Prefer selector-based subscriptions over
useState(wholeObject)
— subscribe only to the slice each component needs.
在使用外部状态时,减少React不必要重渲染的模式。优先使用基于选择器的订阅方式,而非
useState(整个对象)
—— 仅订阅每个组件所需的状态片段。

Core idea

核心思想

Storing a full state object in React state (e.g.
useState(snapshot)
and subscribing to every change) forces re-renders on any update. A component that only needs
phase
will still re-render when
quiz.selectedWrong
changes if both live in the same object.
Avoid: subscribe →
setState(fullObject)
→ 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.
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
App.tsx
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.

将完整的状态对象存储在React状态中(例如
useState(snapshot)
并订阅所有变更)会导致任何更新都触发重渲染。如果某个组件只需要
phase
字段,但
quiz.selectedWrong
字段发生变更时(两者在同一个对象中),该组件仍会重渲染。
避免: 订阅 →
setState(完整对象)
→ 在渲染中读取字段。
推荐: 订阅选择器状态片段,这样组件仅在该值变更时才重渲染。以下所有库都支持此方式,请务必使用。
组件树位置很重要。组件在树中的层级越高,重渲染的成本就越高,因为React会重渲染该组件及其所有后代。在
App.tsx
中订阅整个存储尤其糟糕——存储的任何变更都会导致整个应用重渲染。将订阅推送到实际需要数据的叶子组件或路由级组件,或者使用选择器,让高层级组件仅在其所需的状态片段变更时才重渲染。

Library patterns

各库实现模式

XState (actors) —
@xstate/react

XState(角色)——
@xstate/react

Use
useSelector(actor, selector)
so the component re-renders only when the selected value changes.
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-renders
Actor + ref for callbacks: Keep the actor in
useState
(so
useSelector
re-subscribes if the actor is replaced) and in a
useRef
for synchronous access in event handlers:
tsx
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保存在
useState
中(这样当actor被替换时,
useSelector
会重新订阅),同时用
useRef
保存,以便在事件处理程序中同步访问:
tsx
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 ——
@xstate/store-react

Use
useSelector(store, selector)
to subscribe to a slice of store context. Re-renders only when the selected value changes (strict equality by default; optional custom
compare
).
tsx
// 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)
订阅存储上下文的片段。仅当所选值变更时才重渲染(默认使用严格相等比较;可自定义
compare
函数)。
tsx
// 推荐:选择单个字段 —— 仅当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 instead
Use a module-level selector so the function reference is stable (see Selector rules below). For multiple fields, use
useShallow
or pick primitives:
tsx
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
使用模块级选择器以确保函数引用稳定(见下文选择器规则)。如需多个字段,使用
useShallow
或单独选择原始值:
tsx
import { useShallow } from "zustand/react/shallow";
const { count, name } = useStore(useShallow((state) => ({ count: state.count, name: state.name })));

Redux —
react-redux

Redux ——
react-redux

Use
useSelector(selector)
and select the smallest slice needed. Redux uses referential equality; selecting a new object every time forces re-renders.
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
shallowEqual
or a memoized selector:
tsx
import { shallowEqual, useSelector } from "react-redux";

const { phase, step } = useSelector(
  (state) => ({ phase: state.session.phase, step: state.session.step }),
  shallowEqual
);

使用
useSelector(selector)
并选择最小的所需状态片段。Redux使用引用相等性;如果每次选择都返回新对象,会强制触发重渲染。
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>;
对于对象片段,使用
shallowEqual
或记忆化选择器:
tsx
import { shallowEqual, useSelector } from "react-redux";

const { phase, step } = useSelector(
  (state) => ({ phase: state.session.phase, step: state.session.step }),
  shallowEqual
);

Nanostores —
@nanostores/react

Nanostores ——
@nanostores/react

Nanostores 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
useStore
s only the store it needs.

Nanostores的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>;
}
使用mapatom实现细粒度更新,使用computed处理派生值;这样每个组件仅
useStore
它所需的存储。

React 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
useSyncExternalStore
and subscribe to a slice in
getSnapshot
so the component only re-renders when that slice changes.
tsx
// 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的
useSyncExternalStore
,并在
getSnapshot
中订阅状态片段,这样组件仅在该片段变更时重渲染。
tsx
// 推荐: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

选择器规则

  1. 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);
}
  1. 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.
  2. Don’t put expensive derivation in selectors. Heavy work belongs in
    useMemo
    in the component, not in the selector (selectors run often).

  1. 将选择器放在模块级别 —— 不要在组件内联。内联箭头函数每次渲染都会创建新的引用,会破坏相等性检查。
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);
}
  1. 返回原始值或稳定引用。如果选择器每次都返回新的对象/数组,组件会在每次更新时都重渲染。优先返回原始值;如果必须返回对象,使用自定义比较函数。
  2. 不要在选择器中执行昂贵的推导。繁重的计算应放在组件的
    useMemo
    中,而非选择器(选择器会频繁执行)。

Anti-patterns

反模式

Anti-patternWhy it's badFix
setState(fullSnapshot)
in subscribe
Every store/actor change re-rendersUse selector / slice (useSelector, selector arg, computed store)
No selector / whole store in hookSame as abovePass selector to useStore/useSelector; or use computed/small stores
Inline selector functionNew reference each renderModule-level selector
Selector returns new object every timeAlways re-rendersReturn primitive or use shallowEqual/custom compare
Mega-context with everythingAny update re-renders all consumersSplit context or put a store in context and select in consumer

反模式危害修复方案
在订阅中使用
setState(完整快照)
每次存储/角色变更都会触发重渲染使用选择器/状态片段(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.
    App.tsx
    , layout, root route) — re-renders there cascade down the whole tree, so avoid subscribing to the whole store at that level
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确认。