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是你必须时刻注意并提防的隔壁老王。