React Hooks: Memoization

1,114 阅读5分钟

TL;DR

React hooks 带来了很多开发便利,然而对于对性能有要求或追求的开发者,如何更好地利用hooks呢?

该不该Memoization?

React已经足够轻快,用不用Memoization需要权衡下RIO。想量化react应用性能,首先需要熟悉好React Profiler。Memoization非常适于提高render性能问题。

React.memo 是一性能优化工具,一个HOC。 类似React.PureComponent 但只是一个FC而非CC. 如果输入同样props你的FC render同样结果, React 将 memoize, 跳过渲染component,并复用上次渲染结果.

By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.

不需要Memoization

看个反例

function List({ items }) {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
}
export default function App() {
  log('renderApp');
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}

每次点击inc,renderApp 与 renderList将被打印,即便List啥也没变动。如果list足够大,很可能导致性能瓶颈,因此需要减少没必要的渲染。

简单的Memoization

const List = React.memo(({ items }) => {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
});
export default function App() {
  log('renderApp');
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}

此时memoization明显地减少了render次数。在mount期间renderApp与renderList被打印,但当inc被点击时只有renderApp被打印。

Memoization & callback

咱们在作一个拓展:为所有list items添加一个inc按钮。注意给memoized组件传递callback可能会导致微小的bug。

function App() {
  log('renderApp');

  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));

  return (
    <div>
      <div style={{ display: 'flex' }}>
        <h1>{count}</h1>
        <button onClick={() => setCount(count + 1)}>
          inc
        </button>
      </div>
      <List
        items={items}
        inc={() => setCount(count + 1)}
      />
    </div>
  );
}

此时我们的momoization失效了。由于我们利用了内联lanbda表达式,每次渲染新的引用就被创建。在momoize组件之前,我们需要一种momoize函数本身的方案,这正是useCallback的用武之地。。。

useCallback

useMemo is useful for expensive calculations, useCallback is useful for passing callbacks needed for optimized child components. useMemo与useCallback是React提供的两个实用好帮手,通常useMomo用于缓存一些昂贵的计算,useCallback用于优化自组件所传递的callback。

function App() {
  log('renderApp');

  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));

  const inc = useCallback(() => setCount(count + 1));

  return (
    <div>
      <div style={{ display: 'flex' }}>
        <h1>{count}</h1>
        <button onClick={inc}>inc</button>
      </div>
      <List items={items} inc={inc} />
    </div>
  );
}

此时,我们的memoziation再次失效了!每次inc被点击renderList就被执行。useCallback的默认行为是无论何时新的函数实例被传递时计算新值。由于内联lambada表达式每次render就会创建new instance,因此useCallback的默认配置在此时毫无用处。那么,又该如何处理呢?

1.useCallback with input

const inc = useCallback(() => setCount(count + 1), [count]);

useCallback接收第二参数,一个值数组且一旦这些值变动useCallback将返回新值。此例子里每次count变动,useCallback将返回新的引用。由于count每次render都在变,useCallback将每次render都返回新值。因此该段code并未momizie。

2. useCallback with input of empty array

const inc = useCallback(() => setCount(count + 1), []);

useCallback 可以拿空数组作为输入,此时将在内联lambda里仅执行一次且缓存引用在后继的调用里。 这段代码做了缓存,当点击任意按钮时,一个renderApp将被调用,主要inc按钮将正确工作,但内联inc按钮将不会正常工作。

counter将从0增加到1并不再变化。lambda被一次创建,但多次调用。由于当lambda被创建时count 为0,它的行为完全等价于如下代码:

const inc = useCallback(() => setCount(1), []);

问题的根源在于我们在同一时间内,试图read and write from and to the state。我们需要一个实现此目的的API,幸好React提供两种供大家选择。

2.1 useState with functional updates

const inc = useCallback(() => setCount(c => c + 1), []);

useState返回的setters的参数可以是函数,在此可以读取上一次给定的值。

2.2 useReducer

const [count, dispatch] = useReducer(c => c + 1, 0);

useReducer memoization works exactly as useState in this case. Since dispatch is guaranteed to have same reference across renders, useCallback is not needed, which makes code less error-prone to memoization related bugs. 在此useReducer 缓存作用与useState一样。由于dispatch被保证了在每次render时拥有同样的引用,因此useCallback就不需要了,这也将code变得不容易出memoization类错。

useReducer vs useState

useReducer更适合管理包含很多sub-value的state对象或者下一个state依赖与上一个的情况。通常使用useReducer的姿势是结合useContext去避免在一个大的组件树里显式传递callback。我推荐的thumb原则是最大限度地使用useState去管理不离开组件的数据,但如果父子组件之间需要不离开的双向的data交换,useReducer是更好的选择。

总之, React.memo 和 useReducer 是好搭档, React.memo 和 useState 是偶尔可能出问题而小打小闹的兄弟 , useCallback是你必须时刻注意并提防的隔壁老王。