你有没有遇到过用MobX写React页面的时候,明明只改了一个小小的状态,整个页面或者一堆列表子组件都跟着重渲染,页面卡得半天没反应?这其实是很多刚用MobX的开发者都会踩的坑——MobX的自动响应式默认会收集组件用到的所有Store状态,只要任何状态变了,组件就会重渲染,哪怕和你没关系。今天就聊聊在MobX状态管理架构下,怎么一步步优化组件的渲染性能,让你的页面跑起来更流畅。

一、为什么MobX会出现不必要的组件渲染

1.1 MobX的默认响应逻辑

MobX的核心是“自动追踪依赖”:当你把组件用observer包裹后,组件内所有用到的Store属性都会被MobX标记为“依赖项”,只要这些依赖项有变化,组件就会重新执行render函数。比如你写一个Todo列表,把每个Todo项做成子组件,父组件传整个Store给子组件,那子组件的observer会认为用到了整个Store,只要Store里任何属性变了,哪怕是其他Todo项的内容变了,当前Todo项也会跟着重渲染,这就是无效渲染的来源。举个反面例子:比如父组件传了包含所有Todo的数组,子组件的render里遍历这个数组,这时候哪怕只有一个Todo变了,所有子组件都会重渲染,非常浪费性能。

1.2 常见的性能损耗场景

除了刚才说的“传整个Store给子组件”,还有几种常见的坑:比如把大对象直接传给子组件做props,子组件用了整个对象,那对象的任何属性变了都会触发子组件重渲染;还有在组件render里直接计算复杂数据,每次render都计算,哪怕数据没变化;另外,多人协作写代码的时候,把状态都塞到同一个Store里,导致很多不相关的组件都订阅了同一个大Store,状态一变化,所有相关组件都重渲染。

二、优化MobX组件渲染的具体方法

这部分是核心,每个方法都有示例,大家可以直接套用到自己的项目里。

2.1 精准依赖收集:只订阅需要的状态

MobX的observer虽然会自动收集依赖,但它是“全量”的,我们可以通过只访问需要的属性来缩小依赖范围,让MobX只追踪真正会影响组件渲染的状态。比如刚才的TodoItem子组件,之前可能是这样写的(错误示例):

// 技术栈:React + MobX 6
import { observer } from 'mobx-react-lite';

// 错误写法:依赖了整个todo对象,哪怕只用到content属性,只要todo的其他属性(比如id)变了也会重渲染
const BadTodoItem = observer(({ todo }) => {
  return <li>{todo.content}</li>;
});

export default BadTodoItem;

正确的写法是,虽然只访问了todo.content,但MobX会自动识别只需要这个属性,所以只有content变化的时候才会重渲染,哪怕整个todo对象的其他属性变了,也不会触发:

// 技术栈:React + MobX 6
import { observer } from 'mobx-react-lite';

// 正确写法:只访问需要的content属性,MobX只会追踪这个依赖,只有它变了才重渲染
const GoodTodoItem = observer(({ todo }) => {
  return <li>{todo.content}</li>;
});

export default GoodTodoItem;

这里要注意,不要在组件里做多余的取值,比如不要写const item = todo然后用item.content,反而要直接写todo.content,让MobX精准识别依赖,这个小细节很多开发者容易忽略。

2.2 拆分Store与使用computed属性

很多初学者会把整个应用的状态都塞到一个大Store里,这会导致整个应用的组件都订阅同一个大Store,状态一变化就全页面重渲染。我们可以把Store拆分成多个小模块,比如UserStoreTodoStoreCartStore,每个Store只管理自己的状态,这样组件只订阅自己需要的Store模块,不会被无关状态触发渲染。另外,MobX的computed属性是优化性能的关键:它是基于依赖缓存的,只有当依赖的状态变化时才会重新计算,不会每次render都重复计算。举个例子,计算Todo列表里已完成的数量:

// 技术栈:React + MobX 6
import { makeAutoObservable, computed } from 'mobx';

// 拆分后的TodoStore,只管理Todo相关状态
class TodoStore {
  todos = []; // 只存Todo项的数组
  userStore; // 注入UserStore,不管理用户状态

  constructor(userStore) {
    this.userStore = userStore;
    makeAutoObservable(this); // 自动追踪状态
  }

  // computed属性:只有todos变化时才重新计算,缓存结果,避免重复计算
  get completedTodoCount() {
    return this.todos.filter(todo => todo.done).length;
  }

  addTodo = (content) => {
    this.todos.push({ id: Date.now(), content, done: false });
  }

  toggleTodo = (todoId) => {
    const todo = this.todos.find(t => t.id === todoId);
    if (todo) todo.done = !todo.done;
  }
}

export default TodoStore;

然后在组件里使用这个completedTodoCount的时候,只有todos变化时才会重新计算,哪怕userStore里的用户信息变了,也不会触发计算,间接减少了组件的重渲染。

2.3 搭配React.memo做额外缓存

React的memo是用来缓存组件的,当组件的props没有变化时,不会触发重渲染,它会浅比较props。如果把observermemo结合起来,效果会更好:MobX负责状态变更的精准追踪,memo负责props层面的缓存,双重保障不会出现不必要的渲染。举个示例:

// 技术栈:React + MobX 6
import { observer } from 'mobx-react-lite';
import { memo } from 'react';

// 用memo包裹observer组件,只有两种情况会重渲染:
// 1. 组件的props发生变化(浅比较)
// 2. 组件内用到的MobX依赖项发生变化
const OptimizedTodoItem = memo(observer(({ todo, onToggle }) => {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.content} {todo.done ? '✅' : '❌'}
    </li>
  );
}));

export default OptimizedTodoItem;

这里的onToggle是父组件传的回调函数,要注意不要在父组件的render里创建新的函数,否则memo的浅比较会认为props变化,导致子组件重渲染,所以父组件的回调要定义成固定的,比如用useCallback,这也是React的基础优化技巧,结合MobX用效果更好。

2.4 避免在render里创建临时对象

很多开发者喜欢在组件的render函数里直接创建新对象或数组,比如:

// 错误示例:每次render都会创建新的filteredTodos数组,哪怕todos没变
const BadTodoList = observer(() => {
  const todoStore = useContext(TodoStoreContext);
  // 这里每次render都创建新数组,导致子组件的props变化,哪怕todo内容没变
  const filteredTodos = todoStore.todos.filter(t => t.done);
  return (
    <ul>
      {filteredTodos.map(todo => <OptimizedTodoItem key={todo.id} todo={todo} onToggle={todoStore.toggleTodo} />)}
    </ul>
  );
});

正确的做法是,把这个过滤的逻辑放到Store的computed属性里,这样只有todos变化时才会计算一次,不会每次render都创建新数组:

// 正确做法:在Store里做computed过滤
class TodoStore {
  todos = [];
  get completedTodos() {
    return this.todos.filter(t => t.done);
  }
  // ...其他代码
}

// 组件里直接使用computed属性,不会有临时对象的问题
const GoodTodoList = observer(() => {
  const todoStore = useContext(TodoStoreContext);
  return (
    <ul>
      {todoStore.completedTodos.map(todo => <OptimizedTodoItem key={todo.id} todo={todo} onToggle={todoStore.toggleTodo} />)}
    </ul>
  );
});

三、应用场景、技术优缺点、注意事项

3.1 应用场景

这种优化方案特别适合复杂中后台系统,比如有大量列表的订单管理系统、商品管理后台,这些系统的页面通常包含几十个甚至上百个列表项,每个列表项都是独立的子组件,优化后可以避免页面卡顿;另外,表单联动复杂的页面,比如用户信息表单、电商结算页,多个组件关联不同状态,精准依赖可以只让变化的组件重渲染,提升操作流畅度。

3.2 技术优缺点

优点:相比Redux,MobX的灵活性更高,优化后的渲染性能更好,不用处理Redux的action、reducer等繁琐的流程,适合中小到大型项目;缺点:如果不注意依赖收集,很容易出现隐性的性能问题,比如整个组件订阅大Store,调试的时候需要专门看MobX的依赖追踪,对新手不太友好;另外,过度拆分Store也会增加项目的复杂度,需要合理规划Store的结构。

3.3 注意事项

  1. 不要用observer包裹整个页面组件,应该拆分到子组件,比如页面的Header、Todo列表、侧边栏各自用observer包裹,这样一个子组件的重渲染不会影响其他部分;
  2. 不要在observer组件里做异步请求或副作用,副作用应该放在组件外部(比如useEffect),否则会导致意外的渲染;
  3. 计算属性不要写得太复杂,不要嵌套多个computed,不然缓存的优势会被抵消,甚至出现性能问题;
  4. 调试性能的时候,可以用MobX的工具(比如mobx-devtools)查看每个组件的依赖变化,找到不必要的重渲染原因。

四、总结

在MobX架构下优化组件渲染的核心思路,就是缩小依赖范围减少不必要的计算/渲染:通过精准收集组件的依赖,只让变化的部分重渲染;拆分Store和使用computed属性,把状态和计算逻辑拆分成小模块;搭配React的memouseCallback,进一步减少props层面的无效渲染;最后注意Store的结构和组件的拆分,避免整个大Store的依赖泄露。只要掌握这些方法,就能解决MobX项目里常见的渲染卡顿问题,让你的前端页面跑起来更流畅。