react性能优化-重新render

props与memo

如下所示例子中,因为App内部状态的更新,总会牵连其无辜子组件Demo的更新。

const Demo = () => {
  console.log('Demo render');
  return (
    <div>
      我是子组件
    </div>
  );
};

const App = () => {
  console.log('App render');

  const [count, setCount] = useState(0);
  const doInc = () => {
    setCount(count + 1);
  };

  return (
    <>
      <div>{count}</div>
      <button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
      <hr className='mt-2'/>
      <Demo />
    </>
  );
};

600

如何避免呢? 其实官方已经提供了这个优化的方案,那就是使用memo,你仅仅且包裹住这个组件即可!
memo的作用就是对比组件重新render前后的 props是否发生变化:如果没有发生变化,则不会重新render。

const Demo = memo(() => {
  console.log('Demo render');
  return (
    <div>
      我是子组件
    </div>
  );
});

600

但是注意memo 仅比较基本类型,如果是对象则会比较引用地址,如下则每次还都会重新render:

const Demo = memo((props) => {
  console.log('Demo render');
  return (
    <div>
      我是子组件
    </div>
  );
});

const App = () => {
  console.log('App render');

  const [count, setCount] = useState(0);
  const doInc = () => {
    setCount(count + 1);
  };

  const info = {title: '子组件'};

  return (
    <>
      <div>{count}</div>
      <button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
      <hr className='mt-2'/>
      <Demo info={info}/>
    </>
  );
};

原因也很简单,因为每次App组件的重新render,就会导致info是一个新创建的对象,引用地址自然就不同了!
解决办法:自己做对象比较就行!

import { memo, useState } from 'react';
import isEqual from "lodash/isEqual";

const Demo = memo(() => {
  console.log('Demo render');
  return (
    <div>
      我是子组件
    </div>
  );
},(prev, next)=>isEqual(prev, next));

const App = () => {
  console.log('App render');

  const [count, setCount] = useState(0);
  const doInc = () => {
    setCount(count + 1);
  };

  const info = {title: '子组件'};

  return (
    <>
      <div>{count}</div>
      <button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
      <hr className='mt-2'/>
      <Demo info={info}/>
    </>
  );
};

如果你觉得麻烦,还可以使用另一种方式来规避这种情况。
使用useMemo包裹住这个对象即可,这样这个对象就会被缓存下来不会被App重建!

const Demo = memo((props) => {
  console.log('Demo render');
  return (
    <div>
      我是子组件
    </div>
  );
});

const App = () => {
  console.log('App render');

  const [count, setCount] = useState(0);
  const doInc = () => {
    setCount(count + 1);
  };

  const info = useMemo(()=>({title: '子组件'}),[]);

  return (
    <>
      <div>{count}</div>
      <button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
      <hr className='mt-2'/>
      <Demo info={info}/>
    </>
  );
};

tips: useMemo一般用于缓存对象,如果是函数react还提供了useCallback,作用一样!

useCtx与memo

有时候父子尤其是跨级传递参数,我们并不想通过props传递,此时便是使用useContext的时候

// 定义store
export const defaultValue = {
  count: 0
};
export const AppCtx = createContext(null);

export function useAppCtx() {
  const ctx = useContext(AppCtx);
  return ctx;
}

// 子组件
// 子组件必须包裹memo,因为父组件状态变态变更会牵连到它,要去除这种牵连
const Demo = memo(() => { 
  console.log('Demo render');
  const { app } = useAppCtx(); // 要不要重新render跟app没关系,哪怕不写左侧内容
  
  return <div>
    我是子组件
    <div>{JSON.stringify(app)}</div>
  </div>;
});

// 父组件
const App = () => {
  console.log('App render');

  const [app, setApp] = useState(defaultValue);
  const doInc = () => {
    setApp({ ...app,  count: app.count+1});
  };

  return (
    <AppCtx value={ app }>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变count
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

但是我们会遇到一个问题:因为设置app每次都为一个新的对象,这导致useContext认为是新的值,从而导致子组件重新渲染!
如下,哪怕在app中我没次都设置count的值为初始值(即从始至终 app内容都没变过),但是子组件依然会重新渲染!

const App = () => {
  console.log('App render');

  const [app, setApp] = useState(defaultValue);
  const doInc = () => {
    setApp({ ...app,  count: 0});
  };

  return (
    <AppCtx value={ app }>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

其实解决的办法也还是通过useMemo来解决

// 父组件
const App = () => {
  console.log('App render');

  const [app, setApp] = useState(defaultValue);
  const doInc = () => {
    setApp({ ...app,  count: 0});
  };

  // 如此appCtxValue对象根据条件做缓存(即固化)
  const appCtxValue =useMemo(() => ({
    app
  }), [app.count]); 

  return (
    <AppCtx value={ appCtxValue }>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

memo的比较规则

useMemo(以及 useCallbackuseEffect 等所有 Hook 的 deps 数组)只认“严格相等(Object.is)”这一层,它不会、也永远不会对“对象里的字段”做深度比较。
所以“内容一样但引用不同”的对象,在 deps 里一定会被认为是“变了”,于是 useMemo 会重新执行。


一、为什么设计成这样

  1. 可预测性:
    如果 React 帮你做深比较,内部就要遍历所有字段,遇到循环引用、函数、Symbol 等情况还得特殊处理,结果仍然可能和开发者预期不一致。
  2. 性能:
    深比较成本远高于“全等比较”,而 deps 数组在每次渲染时都会被遍历一次;
    把“是否相等”的决定权交给开发者,可以让大家在“稳定引用”与“重新计算”之间自由权衡。
  3. 数据流哲学:
    React 希望你把“变化”建模成不可变数据——只要语义没变,就复用同一个引用;语义变了,就生成新引用。这样 === 本身就等价于“语义是否变化”。

二、常见“踩坑”复现

function Comp() {
  const [count, setCount] = useState(0);

  // 每次渲染都生成新的「相同内容」的对象
  const params = { page: 1, size: 10 };

  const data = useMemo(() => {
    console.log('重新 fetch');
    return api.fetch(params);
  }, [params]);   // ← 这里永远是“新引用”,所以 useMemo 永远失效

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>rerender</button>
      <div>{data}</div>
    </>
  );
}

控制台会看见“重新 fetch”随每次点击都打印,尽管 page/size 根本没变。


三、4 种正统解决方式

  1. 把对象拆成原始值
    最简单、零成本,React 官方推荐:
const data = useMemo(() => api.fetch({ page, size }), [page, size]);
  1. useMemo 把“对象引用”本身稳住
    只要字段没变,就给它同一个引用:
const params = useMemo(() => ({ page: 1, size: 10 }), []); // 空 deps => 永不变
const data   = useMemo(() => api.fetch(params), [params]);
  1. useDeepCompareMemo 之类自定义 Hook(内部帮你深比较)
    社区已有轮子:use-deep-compare-effectuse-deep-compare-memo
    原理:第一次必执行;后续用深比较决定是否替换 deps,从而触发真正的 React hook。
import useDeepCompareMemo from 'use-deep-compare-memo';

const data = useDeepCompareMemo(() => api.fetch(params), [params]);
  1. 用 immer / zustand / redux-toolkit 等“不可变更新”工具
    只要业务数据语义没变,就天然返回旧引用;语义变了就返回新引用,
    这样直接用 === 就能判断,无需深比较。
// 父组件
const App = () => {
  console.log('App render');

  const [app, setApp] = useImmer(defaultValue); 
  const doInc = () => {
    // 当defaultValue为对象时,仅仅当内容发生变化时才会设置为新对象,否则依然是原对象
    setApp(draft => { 
      draft.count = 0;
    });
  };


  return (
    <AppCtx value={ app }>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

四、一句话总结
useMemo 的 deps 只浅比较引用
想让“内容相同就跳过计算”,要么拆成原始值,要么维持对象引用稳定,要么用自定义 Hook 做深比较
这是 React 不可变数据模型的核心约定,不是“缺陷”,而是“设计如此”。

react-compiler

近期(2025年)提出了一个新的优化性能方式:react-compiler
只要配置好这个,你无需任何手动使用memo、useMemo、useCallback优化,react会自己优化!

官方说法:React Compiler 会自动为你完成这项优化,让你从这些繁琐的负担中解脱出来,专注于功能构建。

posted @ 2025-09-24 12:50  丁少华  阅读(14)  评论(0)    收藏  举报