React Context 性能陷阱:一个值变了,凭什么整棵树都重渲染你用 Context 存了全局状态,页面越做越卡,打开 Profiler 一看:改一个用户名,几十个跟用户名八竿子打不着的组件全部重渲染了。这不是你的错觉,这是 Context 的默认行为——Provider 的 value 一变,所有消费这个 Context 的组件全部重渲染,不管它们用没用到变化的那部分。这篇讲清楚为什么会这样,以及三种能落地的修法。先复现这个坑import { createContext, useContext, useState } from react; const AppContext createContext(null); function App() { const [user, setUser] useState({ name: Ann }); const [theme, setTheme] useState(light); // 每次 render 都新建一个对象字面量! return ( AppContext.Provider value{{ user, setUser, theme, setTheme }} UserPanel / ThemePanel / /AppContext.Provider ); } function ThemePanel() { const { theme } useContext(AppContext); console.log(ThemePanel 渲染); // 改 user 时,这里也会打印! return div当前主题:{theme}/div; }现象:点按钮只改user,ThemePanel明明只用了theme,却跟着重渲染了。有两个叠加的原因。原因一:value 每次都是新对象value{{ user, setUser, theme, setTheme }}这个对象字面量,每次App渲染都会新建一个引用不同的对象。React 判断 Context 是否变化用的是Object.is比较引用,新对象引用永远不等于旧的,于是所有消费者都被通知「变了」。哪怕你App里改的是完全无关的 state,只要App重渲染,这个 value 对象就重建,所有useContext(AppContext)的组件全部跟着渲染。第一步修复:用useMemo稳定 value 的引用。import { useMemo } from react; function App() { const [user, setUser] useState({ name: Ann }); const [theme, setTheme] useState(light); // 只有 user/theme 真变了才生成新对象,否则复用同一个引用 const value useMemo( () ({ user, setUser, theme, setTheme }), [user, theme] ); return ( AppContext.Provider value{value} UserPanel / ThemePanel / /AppContext.Provider ); }setUser/setTheme是useState返回的,本身引用稳定,不用进依赖数组。但这只解决了「无关 state 变化导致的重渲染」。还有一个更根本的问题。原因二:同一个 Context 混装了变化频率不同的数据就算加了useMemo,当你真的改user时,value对象确实变了(因为 user 变了),于是连只用 theme 的 ThemePanel 还是会重渲染。因为 Context 的粒度是「整个 value 对象」,而不是「对象里的某个字段」。React 没有内置的「只订阅 value.theme」这种细粒度订阅。根本解法:按变化频率把一个大 Context 拆成多个小 Context。用户信息和主题是两码事,变化频率也不同,就别塞一个 Context 里。const UserContext createContext(null); const ThemeContext createContext(null); function App() { const [user, setUser] useState({ name: Ann }); const [theme, setTheme] useState(light); const userValue useMemo(() ({ user, setUser }), [user]); const themeValue useMemo(() ({ theme, setTheme }), [theme]); return ( UserContext.Provider value{userValue} ThemeContext.Provider value{themeValue} UserPanel / ThemePanel / /ThemeContext.Provider /UserContext.Provider ); } function ThemePanel() { const { theme } useContext(ThemeContext); // 只订阅 ThemeContext return div当前主题:{theme}/div; // 改 user 时它纹丝不动 }现在改user只会让订阅UserContext的组件重渲染,ThemePanel因为只订阅ThemeContext,完全不受影响。拆分 Context 是最符合 React 心智模型、也最有效的办法。一个经典拆法:state 和 dispatch 分开用useReducer做全局状态时,有个很实用的拆分模式:把 state 和 dispatch 放到两个 Context。因为dispatch的引用永远稳定,只需要「触发动作」不需要读 state 的组件(比如一个「加一」按钮),订阅 dispatch 那个 Context 就永远不会因为 state 变化而重渲染:const StateContext createContext(null); const DispatchContext createContext(null); function CounterProvider({ children }) { const [state, dispatch] useReducer(reducer, { count: 0 }); return ( StateContext.Provider value{state} {/* dispatch 引用天然稳定,这个 Provider 的 value 永不变 */} DispatchContext.Provider value{dispatch} {children} /DispatchContext.Provider /StateContext.Provider ); } // 只发动作、不读值的组件:订阅 dispatch,count 怎么变它都不重渲染 function AddButton() { const dispatch useContext(DispatchContext); return button onClick{() dispatch({ type: inc })}1/button; } // 需要显示值的组件:订阅 state function CountLabel() { const state useContext(StateContext); return span{state.count}/span; }AddButton点一万次,它自己一次都不会因为 count 变化而重渲染,因为它订阅的DispatchContext的 value(dispatch)从头到尾是同一个引用。还有个隐藏点:children 别一起重渲染上面CounterProvider用{children}而不是把子组件直接写在里面,这也是有讲究的。children是从外部传进来的 props,CounterProvider自身重渲染时,children这个已经创建好的 element 引用不变,React 会跳过它们的重渲染。如果你把AddButton /直接硬写在 Provider 里,Provider 一渲染它们就跟着重建了。这条对「把 Provider 单独抽成组件」很关键。小结Context 的默认行为:Provider 的 value 引用一变(Object.is比较),所有消费者全部重渲染,不看你用没用到变化的字段。第一步永远是给 value 加useMemo,避免「对象字面量每次新建」导致的无谓重渲染——但它治不了「真改了 value 里某个字段」的连带渲染。根本解法是按变化频率拆分 Context:不相干、变化频率不同的数据(user vs theme)放不同 Context,各自订阅、互不牵连。经典模式:state 与 dispatch 分成两个 Context,只发动作的组件订阅稳定的 dispatch,永不因 state 变化重渲染;Provider 抽成组件并透传children还能少渲一层。一句话记忆点:Context 订阅的是整个 value 不是字段;想少渲染,要么 useMemo 稳引用,要么干脆拆成多个小 Context。