如何减少 React 组件的无意义重复渲染?

一、先搞懂:组件为啥会无意义重渲染?

React 组件重渲染的核心触发条件是:
  1. 组件自身 state 变化;
  2. 父组件重渲染(即使传给子组件的 props 没变化);
  3. 组件使用的 context 数据变化;
  4. 用了 useState/useReducer 但依赖没写对。
「无意义重渲染」就是:组件满足上述条件,但没有任何 UI 需要更新(比如父组件点了个按钮,子组件没用到父组件的任何数据,却跟着渲染)。

二、核心解决方案(按优先级排序,越靠前越易落地)

1. 用 React.memo 缓存纯组件(最常用)

React.memo 是高阶组件,会浅比较子组件的 props,只有 props 真变化时才重渲染。
  • 适用场景:子组件是「纯组件」(输出只由 props 决定,无内部 state / 副作用)。
  • 错误示范:父组件重渲染,子组件无意义跟着渲染
    jsx
     
     
     
     
     
    // 父组件
    function Parent() {
      const [count, setCount] = useState(0);
      // 每次渲染都会创建新的handleClick函数(引用变化)
      const handleClick = () => setCount(count + 1);
      return (
        <div>
          <button onClick={handleClick}>点击{count}次</button>
          {/* 即使msg没变化,子组件也会跟着父组件渲染 */}
          <Child msg="固定文本" />
        </div>
      );
    }
    // 子组件:无memo,父渲染则子渲染
    function Child({ msg }) {
      console.log("子组件无意义渲染了!");
      return <div>{msg}</div>;
    }
     
     
  • 优化方案:用 memo 包裹子组件
    jsx
     
     
     
     
     
    // 子组件:加memo,仅props变化时渲染
    const Child = memo(({ msg }) => {
      console.log("子组件只在props变化时渲染!");
      return <div>{msg}</div>;
    });
     
     
  • 注意memo 是「浅比较」,如果 props 是对象 / 数组(引用类型),即使内容没变化,引用变了也会触发渲染(后续用 useCallback/useMemo 解决)。

2. 用 useCallback 缓存函数型 props

父组件传给子组件的函数,每次渲染都会创建新引用,即使 memo 也会失效 —— 用 useCallback 缓存函数引用,只有依赖变化时才重新创建。
  • 错误示范:memo 失效(函数 props 引用变化)
    jsx
     
     
     
     
     
    function Parent() {
      const [count, setCount] = useState(0);
      // 每次渲染创建新函数,Child的memo失效
      const handleChildClick = () => console.log("点击子组件");
      
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>点击{count}次</button>
          <Child onClick={handleChildClick} />
        </div>
      );
    }
    const Child = memo(({ onClick }) => {
      console.log("memo失效,还是渲染了!");
      return <button onClick={onClick}>子组件按钮</button>;
    });
     
     
  • 优化方案:用 useCallback 缓存函数
    jsx
     
     
     
     
     
    function Parent() {
      const [count, setCount] = useState(0);
      // 缓存函数,只有依赖(空数组)变化时才重新创建
      const handleChildClick = useCallback(() => {
        console.log("点击子组件");
      }, []); // 依赖为空,函数永远不重新创建
      
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>点击{count}次</button>
          <Child onClick={handleChildClick} />
        </div>
      );
    }
    const Child = memo(({ onClick }) => {
      console.log("只有onClick变化时才渲染!");
      return <button onClick={onClick}>子组件按钮</button>;
    });
     
     

3. 用 useMemo 缓存对象 / 数组型 props

和函数同理,对象 / 数组每次渲染会创建新引用,导致 memo 失效 —— 用 useMemo 缓存引用类型 props。
  • 错误示范:对象 props 引用变化导致 memo 失效
    jsx
     
     
     
     
     
    function Parent() {
      const [count, setCount] = useState(0);
      // 每次渲染创建新对象,Child的memo失效
      const userInfo = { name: "张三", age: 20 };
      
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>点击{count}次</button>
          <Child user={userInfo} />
        </div>
      );
    }
    const Child = memo(({ user }) => {
      console.log("对象引用变了,memo失效!");
      return <div>{user.name}</div>;
    });
     
     
  • 优化方案:用 useMemo 缓存对象
    jsx
     
     
     
     
     
    function Parent() {
      const [count, setCount] = useState(0);
      // 缓存对象,依赖为空则引用不变
      const userInfo = useMemo(() => ({ name: "张三", age: 20 }), []);
      
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>点击{count}次</button>
          <Child user={userInfo} />
        </div>
      );
    }
    const Child = memo(({ user }) => {
      console.log("只有user内容变了才渲染!");
      return <div>{user.name}</div>;
    });
     
     

4. 拆分组件,缩小重渲染范围

把大组件拆成「稳定部分」和「变化部分」,让变化部分只影响小范围组件,避免整个大组件重渲染。
  • 错误示范:大组件包含稳定 UI 和变化 UI,一点变化全渲染
    jsx
     
     
     
     
     
    // 大组件:头部(稳定)+ 计数器(变化)+ 列表(稳定)
    function BigComponent() {
      const [count, setCount] = useState(0);
      return (
        <div>
          {/* 稳定UI:每次count变化也会跟着渲染 */}
          <Header title="固定标题" />
          {/* 变化UI */}
          <button onClick={() => setCount(count + 1)}>{count}</button>
          {/* 稳定UI:每次count变化也会跟着渲染 */}
          <List data={[1,2,3]} />
        </div>
      );
    }
     
     
  • 优化方案:拆分出变化组件,稳定组件不受影响
    jsx
     
     
     
     
     
    // 拆分变化部分为独立组件
    function Counter() {
      const [count, setCount] = useState(0);
      return <button onClick={() => setCount(count + 1)}>{count}</button>;
    }
    // 大组件:只包含稳定UI,变化UI拆出去
    function BigComponent() {
      return (
        <div>
          <Header title="固定标题" />
          <Counter /> {/* 只有这个组件会重渲染 */}
          <List data={[1,2,3]} />
        </div>
      );
    }
     
     

5. 优化 Context 使用(避免全局重渲染)

如果用 createContext 传全局数据,一旦 context 值变化,所有消费该 context 的组件都会重渲染 —— 解决方案:
  • 拆分 Context:把高频变化和低频变化的 Context 分开(比如「用户登录状态」和「主题设置」拆成两个 Context);
  • 用 useContext 只在必要组件中消费:不要在父组件消费 Context 再传给子组件,让子组件直接消费(减少父组件重渲染带动子组件)。

6. 用 useRef 替代不必要的 state

如果数据变化不需要触发 UI 更新(比如定时器 ID、DOM 元素、临时数据),用 useRef 代替 useState,避免无意义重渲染。
  • 错误示范:用 state 存不需要 UI 更新的数据
    jsx
     
     
     
     
     
    function Timer() {
      // 定时器ID变化不需要UI更新,用state会触发渲染
      const [timerId, setTimerId] = useState(null);
      const start = () => {
        const id = setInterval(() => console.log("计时"), 1000);
        setTimerId(id); // 触发无意义重渲染
      };
      return <button onClick={start}>开始计时</button>;
    }
     
     
  • 优化方案:用 useRef 存非 UI 数据
    jsx
     
     
     
     
     
    function Timer() {
      const timerId = useRef(null); // 变化不触发渲染
      const start = () => {
        timerId.current = setInterval(() => console.log("计时"), 1000);
      };
      return <button onClick={start}>开始计时</button>;
    }
     
     

三、避坑要点(新手最容易踩的坑)

  1. 不要滥用 memo/useCallback/useMemo
    • 这些 API 本身有「浅比较成本」,如果组件渲染本身很快(<1ms),用了反而增加开销;
    • 只在「重渲染次数多、渲染耗时高」的组件上用(比如大数据列表、复杂表单)。
  2. useCallback/useMemo 的依赖要写全
    • 漏写依赖会导致缓存的函数 / 数据不更新,出现 BUG(可以用 ESLint 规则 react-hooks/exhaustive-deps 强制检查)。
  3. memo 对「内部有 state/useContext 的组件」无效
    • memo 只能阻止「props 不变但父渲染」的情况,如果组件自身 state/context 变化,还是会渲染(这是正常的)。

四、实战验证:用 React Profiler 确认优化效果

优化后按以下步骤验证:
  1. 打开 React DevTools → Profiler,录制组件操作;
  2. 看目标组件的「渲染次数」是否减少(比如从 10 次→1 次);
  3. 看「渲染原因」是否只有「必要的 props/state 变化」(而非「Parent rendered」)。

总结

  1. 核心手段:memo(缓存组件)+ useCallback(缓存函数)+ useMemo(缓存引用类型)是解决无意义重渲染的「三板斧」;
  2. 优化逻辑:让组件只依赖「真正需要的变化数据」,缩小重渲染范围;
  3. 避坑关键:不滥用缓存 API,依赖写全,只优化耗时高的组件。
posted @ 2025-12-24 17:46  Python也不过如此  阅读(0)  评论(0)    收藏  举报