优化:如何避免 React Context 引起的全局挂载节点树重新渲染

最近项目中一个React Context在不断的接受websocket事件,然后一直修改state,导致重复渲染过多,比较卡顿

TM的这状态管理真乱,趁机总结一下React Context的使用的注意事项

React Context 用于在组件树中传递数据,而不必手动地通过 props 逐层传递。然而,它的便利性也带来了一个常见的性能陷阱:当 Context 的值发生变化时,所有依赖该 Context 的消费组件都会重新渲染,即使它们只使用了 Context 值中的一小部分。

如果处理不当,这种全局性的重新渲染可能会拖慢你的应用,尤其是在 Context Provider 位于组件树顶层,并且其值包含频繁变动的数据时。

Context的重新渲染机制

当我们使用 useContext(MyContext)<MyContext.Consumer> 时,React 会在内部建立一个订阅关系。

  1. 当 Provider 的 value 属性发生变化时 :React 会检查新旧值是否严格相等(===)。
  2. 如果值不相等:React 会通知所有订阅了该 Context 的 Consumer 组件执行重新渲染。

React 并没有对 Consumer 实际使用了 Context 值中的哪部分属性 进行细粒度分析。只要 Context 的 value 对象本身 引用发生了变化,所有 Consumer 都会触发更新。

JavaScript 复制代码
// ❌ 常见但易导致全局渲染的模式
const MyContext = createContext({ user: null, settings: {} });

function App() {
  // state 只要更新,value 对象就会创建一个新的引用
  const [appState, setAppState] = useState({ user: { name: 'Gemini' }, theme: 'dark' });

  // 每次 App 渲染,这个对象都是一个新的引用
  const contextValue = useMemo(() => appState, [appState]);

  return (
    <MyContext.Provider value={contextValue}>
      <Header />
      <Content />
      <Footer />
    </MyContext.Provider>
  );
}

// 假设 Header 只使用了 appState.user
function Header() {
  const { user } = useContext(MyContext);
  // ... 其他代码
  return <h1>Welcome, {user.name}</h1>;
}

// 假设 Footer 只使用了 appState.theme
function Footer() {
  const { theme } = useContext(MyContext);
  // ... 其他代码
  return <p>Current theme: {theme}</p>;
}

// ⚡️ 陷阱:即使只有 theme 变化,Header 也会重新渲染!

避免全局重新渲染

我们可以通过以下几种策略,将 Context 的重新渲染范围限制在真正需要更新的组件。

1. 拆分 Context

这是最简单、最有效的策略之一。与其将所有状态都塞入一个"大 Context"中,不如根据数据的更新频率耦合关系将其拆分成多个独立的 Context。

  • 高频更新 / 独立的 Context :例如,用户交互状态(IsLoadingContext)。
  • 低频更新 / 共享的 Context :例如,全局配置和静态数据(ThemeContext)。
JavaScript 复制代码
// ✅ 拆分成多个独立的 Context
const UserContext = createContext(null);
const ThemeContext = createContext(null);

function App() {
  const [user, setUser] = useState({ name: 'Gemini' });
  const [theme, setTheme] = useState('dark');

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Header /> {/* 仅消费 UserContext */}
        <Footer /> {/* 仅消费 ThemeContext */}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// 优化效果:
// 1. user 变化,只有 Header 及其子树可能重新渲染。
// 2. theme 变化,只有 Footer 及其子树可能重新渲染。互不影响。

2. 使用 Custom Hook 和 memo 结合

这种方法适用于你无法拆分 Context,但又想防止 Consumer 重新渲染的情况

通过 useMemo 或自定义 Hook 仅提取 Context 中需要的属性,并结合 React.memo 来跳过不必要的渲染

JavaScript 复制代码
// 针对 Header 组件的自定义 Hook
const useUser = () => {
  const context = useContext(MyContext);
  // 仅返回 user 部分,确保只有 user 改变时才返回新引用
  return useMemo(() => context.user, [context.user]); 
};

// 结合 React.memo
const MemoizedHeader = React.memo(function Header() {
  const user = useUser(); // 即使 MyContext 整体变了,只要 user 不变,useUser 就会返回旧引用

  // ... 渲染逻辑
});

// ⚡️ 陷阱规避:
// 1. MyContext 整体改变,MemoizedHeader 接收新的 props (即 useUser 返回的值)。
// 2. 但由于 useUser() 对 user 属性进行了 useMemo 优化,如果 user 对象引用没有变化,
// 3. React.memo 就会发挥作用,跳过 Header 的渲染。

END

祝大家暴富!!!

相关推荐
森之鸟1 小时前
2026年AI编程工具全景图:GitHub Copilot vs Cursor vs Codeium,我如何选择?
github·copilot·ai编程
天人合一peng2 小时前
Unity中button 和toggle监听事件函数有无参数
前端·unity·游戏引擎
多米Domi0112 小时前
0x3f 第48天 面向实习的八股背诵第五天 + 堆一题 背了JUC的题,java.util.Concurrency
开发语言·数据结构·python·算法·leetcode·面试
方也_arkling3 小时前
别名路径联想提示。@/统一文件路径的配置
前端·javascript
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于web教师继续教育系统的设计与实现为例,包含答辩的问题和答案
前端
wAIxiSeu3 小时前
Github开源项目推荐
开源·github
web打印社区3 小时前
web-print-pdf:突破浏览器限制,实现专业级Web静默打印
前端·javascript·vue.js·electron·html
RFCEO3 小时前
前端编程 课程十三、:CSS核心基础1:CSS选择器
前端·css·css基础选择器详细教程·css类选择器使用方法·css类选择器命名规范·css后代选择器·精准选中嵌套元素
Amumu121384 小时前
Vuex介绍
前端·javascript·vue.js
We་ct4 小时前
LeetCode 54. 螺旋矩阵:两种解法吃透顺时针遍历逻辑
前端·算法·leetcode·矩阵·typescript