React Hooks 性能优化的正确姿势

1,816 阅读4分钟

前言

React Hooks 出来很长一段时间了,相信有不少的朋友已经深度使用了。无论是 React 本身,还是其生态中,都在摸索着进步。鉴于我使用的 React 的经验,给大家分享一下。对于 React hooks,性能优化可以从以下几个方面着手考虑。

场景1

在使用了 React Hooks 后,很多人都会抱怨渲染的次数变多了。没错,官方就是这么推荐的:

我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。

function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
}

这种写法在异步的条件下,比如调用接口返回后,同时 setPositionsetSize,相较于 class 写法会额外多出一次 render。这也就是渲染次数变多的根本原因。当然这种写法仍然值得推荐,可读性和可维护性更高,能更好的逻辑分离。

针对这种场景若出现十几或几十个 useState 的时候,可读性就会变差,这个时候就需要相关性的组件化了。以逻辑为导向,抽离在不同的文件中,借助 React.memo 来屏蔽其他 state 导致的 rerender

const Position = React.memo(({ position }: PositionProps) => {
  // position 相关逻辑
  return (
    <div>{position.left}</div>
  );
});

因此在 React hooks 组件中尽量不要写流水线代码,保持在 200 行左右最佳,通过组件化降低耦合和复杂度,还能优化一定的性能。

场景2

class 对比 hooks,上代码:

class Counter extends React.Component {
  state = {
    count: 0,
  };

  increment = () => {
    this.setState((prev) => ({
      count: prev.count + 1,
    }));
  };

  render() {
    const { count } = this.state;
    return <ChildComponent count={count} onClick={this.increment} />;
  }
}
function Counter() {
  const [count, setCount] = React.useState(0);

  function increment() {
    setCount((n) => n + 1);
  }
  return <ChildComponent count={count} onClick={increment} />;
}

凭直观感受,你是否会觉得 hooks 等同于 class 的写法?错,hooks 的写法已经埋了一个坑。在 count 状态更新的时候, Counter 组件会重新执行,这个时候会重新创建一个新的函数 increment。这样传递给 ChildComponentonClick 每次都是一个新的函数,从而导致 ChildComponent 组件的 React.memo 失效。

解决办法:

function usePersistFn<T extends (...args: any[]) => any>(fn: T) {
  const ref = React.useRef<Function>(() => {
    throw new Error('Cannot call function while rendering.');
  });
  ref.current = fn;
  return React.useCallback(ref.current as T, [ref]);
}
// 建议使用 `usePersistFn`
const increment = usePersistFn(() => {
  setCount((n) => n + 1);
});
// 或者使用 useCallback
const increment = React.useCallback(() => {
  setCount((n) => n + 1);
}, []);

上面声明了 usePersistFn 自定义 hook,可以保证函数地址在本组件中永远不会变化。完美解决 useCallback 依赖值变化而重新生成新函数的问题,逻辑量大的组件强烈建议使用。

不仅仅是函数,比如每次 render 所创建的新对象,传递给子组件都会有此类问题。尽量不在组件的参数上传递因 render 而创建的对象,比如 style={{ width: 0 }} 此类的代码用 React.useMemo 来优化。

const CustomComponent = React.memo(({ width }: CustomComponentProps) => {
  const style = React.useMemo(() => ({ width } as React.CSSProperties), [width]);
  return <ChildComponent style={style} />;
});

style 若不需改变,可以提取到组件外面声明。尽管这样做写法感觉太繁琐,但是不依赖 React.memo 重新实现的情况下,是优化性能的有效手段。

场景3

对于复杂的场景,使用 useWhyDidYouUpdate hook 来调试当前的可变变量引起的 rerender。这个函数也可直接使用 ahooks 中的实现。

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changesObj = {};
      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changesObj).length) {
        console.log('[why-did-you-update]', name, changesObj);
      }
    }
    previousProps.current = props;
  });
}

const Counter = React.memo(props => {
  useWhyDidYouUpdate('Counter', props);
  return <div style={props.style}>{props.count}</div>;
});

useWhyDidYouUpdate 中所监听的 props 发生了变化,则会打印对应的值对比,是调试中的神器,极力推荐。

场景4

借助 Chrome Performance 代码进行调试,录制一段操作,在 Timings 选项卡中分析耗时最长逻辑在什么地方,会展现出组件的层级栈,然后精准优化。

场景5

React 中是极力推荐函数式编程,可以让数据不可变性作为我们优化的手段。我在 React class 时代大量使用了 immutable.js 结合 redux 来搭建业务,与 ReactPureComponnet 完美配合,性能保持非常好。但是在 React hooks 中再结合 typescript 它就显得有点格格不入了,类型支持得不是很完美。这里可以尝试一下 immer.js,引入成本小,写法也简洁了不少。

const nextState = produce(currentState, (draft) => {
  draft.p.x.push(2);
})

 // true
currentState === nextState;

场景6

复杂场景使用 Map 对象代替数组操作,map.get(), map.has(),与数组查找相比尤其高效。

// Map
const map = new Map([['a', { id: 'a' }], ['b', { id: 'b' }], ['c', { id: 'c' }]]);

// 查找值
map.has('a');
// 获取值
map.get('a');
// 遍历
map.forEach(n => n);

// 它可以很容易转换为数组
Array.from(map.values());

// 数组
const list = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];

// 查找值
list.some(n => n.id === 'a');
// 获取值
list.find(n => n.id === 'a');
// 遍历
list.forEach(n => n);

结语

React 性能调优,除了阻止 rerender,还有与写代码的方式有关系。最后,我要推一下近期写的 React 状态管理库 github.com/MinJieLiu/h…,也可以作为性能优化的一个手段,希望大家从 redux 的繁琐中解放出来,省下的时间用来享受生活😀