react性能优化

144 阅读4分钟

减少渲染的节点/降低渲染计算量

0️⃣ 不要在渲染函数里面进行不必要的计算

1️⃣ 减少不必要的嵌套 一般不必要的节点嵌套都是滥用高阶组件/RenderProps 导致的 有很多种方式来代替高阶组件/RenderProps,例如优先使用 props、React Hooks

2️⃣ 虚拟列表 实现方案: 1.intersectionobserver只渲染在可视区的内容 只观察第一个跟最后一个元素,比如一个list,当最后一个元素进入可视区的时候,渲染的第一个元素变成最后一个元素的下一个元素 2.滚动加载

3️⃣ 惰性加载

精细化渲染

  • 简化 props ① 如果一个组件的 props 太复杂一般意味着这个组件已经违背了‘单一职责’,首先应该尝试对组件进行拆解. ② 另外复杂的 props 也会变得难以维护, 比如会影响shallowCompare效率, 还会让组件的变动变得难以预测和调试.

hooks优化

  • useState初始化惰性函数
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:

function Table(props) {
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

initialState以函数形式传入时,它只会在函数组件初始化的时候执行一次,函数re-render时不会再被执行

以上尚待考究;

useState(props.value)

并不会随着props的变化而变化,初始化值只会初始一次,同样的useRef也是一样

优化key

除了以下的情况不使用index作为key: 列表的重新排序,增删,过滤不会发生

减少re-render

哪些原因会导致rerender

  • 状态更改

    • 当组件的状态发生变化时,它将重新渲染自身。需要注意的是,类组件的state被重复设置成同一个值会导致重新渲染,但是函数组件不会。
  • 父级(或子级)重新渲染

    注意是父组件的重新渲染触发了子组件的重新渲染,与子组件的 props 是否变化无关。只有那些使用了 React.memo 和 useMemo 的组件,props 的变化才会触发组件的重新渲染。

  • context 变化

  • hooks 变化。

    • hooks 内部的状态变化会触发组件的 re-render
    • 如果 hooks 使用了 context,并且 context 的值发生了变化,也会触发组件的 re-render

怎么避免re-render

  • 避免在 render 函数中创建组件,原因如下: 在每次重新渲染时,React 都会重新装载这个组件(即销毁它并从头开始重新创建),这比正常的重新渲染要慢得多。除此之外,还会导致以下问题:

    • 在重新渲染期间可能出现内容 “闪烁”
    • 每次重新渲染时在组件的状态会被重置
    • 每次重新渲染时不会触发依赖项的 useEffect
    • 如果组件被聚焦,则焦点将丢失
  • state 下移到子组件中

  • 组件作为 props

与前面的模式基本相同,将状态封装在一个较小的组件中,而较重的组件作为 props 传递给它。props 不受 state 变化的影响,因此该较重的组件不会被重新渲染

image.png

  • 使用 React.memo

常见误区

  • 无用的 useMemo/useCallback props 将子组件的 props 包装成 memoize 值,是不能避免该子组件重新渲染的。只要父组件重新渲染,那么子组件就会被重新渲染,与 props 没有关系。这个时候就需要用到React.Memo

错误做法

image.png

正确做法

image.png

  • 正确使用key 如果列表项是一个组件,父组件重新渲染了,仅仅设置 key 值并不会提高列表的性能。为了避免列表元素的重新渲染,还要用 React.memo 包装它们

image.png

  • 避免 Context 引起 re-render
    • 将 Provider 的值做 memoize 处理

如果 Context Provider 没有在页面的根节点上,那么祖先节点的变化也会导致它被重新渲染,所以它的值也应该被 memoize。

image.png

  • 对数据和 API 做拆分

image.png

  • @rematch/core 使用时:useContext,无需在根组件包裹store,只需要
init({ models: { global } });
  • 使用createPortal创建的组件A,插入到一个dom节点,dom节点所在的组件被重新渲染的话,是不会引发组件A的重新渲染的。

context优化

  • 拆分context
  • useMemo React.memo做缓存
  • 借鉴redux的发布订阅模式
// provider
const Provider = ({ children }) => {
  const [store, dispatch] = useReducer(reducer, initState);
  const storeRef = useRef(null);
  storeRef.current = store;
  // 订阅所有子组件更新的回调函数
  const subscribeRef = useRef([]);
  // store变化的时候执行所有的监听函数
  useEffect(() => {
    subscribeRef.current.forEach((sub) => sub());
  }, [store]);
  // 缓存value 利用ref拿到最新的状态
  const value = useMemo(
    () => ({
      dispatch,
      subscribe: (cb) => {
        subscribeRef.current.push(cb);
        // return unsubscribe
        return () => {
          subscribeRef.current = subscribeRef.current.filter(
            (item) => item !== cb
          );
        };
      },
      getState: () => storeRef.current,
    }),
    []
  );
  // value此后都不会变化了 只有store变化 store变化的时候通知所有的消费者
  return <MyContext.Provider value={value} children={children} />;
};
// useSelector
export const useSelector = (selector) => {
  const [, forceRender] = useReducer((s) => s + 1, 0);
  const store = useContext(Mycontext);
  // 获取state
  const selectedStateRef = useRef(null);
  selectedStateRef.current = selector(store.getState());
  // 更新回调 对比更新后的state
  const checkUpdates = useCallback(() => {
    const newState = selector(store.getState());
    if (newState !== selectedStateRef.current) {
      forceRender();
    }
  }, []);
  // mounted的时候订阅store
  useEffect(() => {
    const unsubscribe = store.subscribe(checkUpdates);
    // 卸载的时候取消订阅
    return unsubscribe;
  }, [store, checkUpdates]);
  // 返回最新的state
  return selectedStateRef.current;
};

useDispatch 截屏2022-11-15 上午11.46.36.png 使用:

截屏2022-11-15 上午11.50.30.png

参考