React Hooks让我们的工作方方面面都变得更好。但是但有时候一些性能问题也是很棘手的。我们可以运用Hooks编写高性能应用,但是前提是你对于下面的这些问题有清醒的认知。
你该不该使用Memoization?
Memoization:在计算机科学中,记忆化(英语:memoization而非memorization)是一种提高程序运行速度的优化技术。通过储存大计算量函数的返回值,当这个结果再次被需要时将其从缓存提取,而不用再次计算来节省计算时间。 记忆化是一种典型的时间存储平衡方案。 --维基百科
React在大多数使用场景中已经足够高性能。如果你的应用足够快并且没有任何渲染问题,那么就没必要往下看了。不要尝试去解决假想的渲染问题,所以在提高性能之前,确认下你是不是熟悉React Profiler
。
如果你已经明确知道加载慢的问题在哪,Memoization是最好的尝试方法。
React.memo
是提高性能工具,同时也是一个HOC(高阶组件)。它和React.PureComponent
很相似,只不过它应用于函数组件而不是class组件。如果函数组件根据给定的相同的props渲染相同的结果,React会记住这些,跳过渲染组件,并且复用最后一次渲染结果。
默认情况下它会对复杂的props对象进行浅比较。如果你想要控制比较过程,也可以提供一个自定义的比较函数作为钩子函数的第二个参数。
无Memoization:
我们来举一个不使用Memoization的例子,然后看为什么这样会造成问题。
function List({ items }) {
log('renderList');
return items.map((item, key) => <div key={key}>item: {item.text}</div>);
}
export default function App() {
log('renderApp');
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>
);
}
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>
);
}
NO1:Live Demo
每次点击inc
,renderApp
和renderList
都会打印,甚至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>
);
}
NO2:Live Demo
在这个例子中Memoization做了很多工作,并且减少了很多渲染次数。在加载过程中renderApp
和renderList
都打印了,但是点击inc
的时候只有renderApp
打印了。
Memoization & callback
我们来做一个小修改,在所有List
条目中添加上inc
按钮。记住,向组件中传入回调函数来记忆化组件会造成微妙的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>
);
}
NO3:Live Demo
在这个例子中,我们的Memoization失败了。因为我们使用了内联匿名函数,每次渲染的时候都会生成新的渲染结果,这样React.mome
就失效了。所以在我们记住这个组件之前,我们要想办法记住这个函数。
useCallback
十分幸运的是,React有两个内建钩子可以提供上面的功能:useMemo
和useCallback
。useMemo
对于复杂计算很有用,useCallback
对于传递回调函数给需要提高性能的子组件十分有用。
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>
);
}
NO4:Live Demo
在这个例子中,我们的Memoization又失败了。renderList
在每次inc
被点击的时候都会调用。useCallback
的默认行为是函数实例每次挂载的时候都计算新值。因为内联匿名函数每次渲染的时候都会创建新实例,默认设置下useCallback
无效。
useCallback with input
const inc = useCallback(() => setCount(count + 1), [count]);
NO5:Live Demo
useCallback
接收的第二个参数是一个输入变量数组,当且仅当这些输入变量变化的时候,useCallback
会返回新值。在这个例子中,当count
变化的时候useCallback
会返回新值。每次count
在渲染的时候改变,useCallback
都会跟着改变。这样也没有实现很好的记忆功能。
useCallback with input of empty array
const inc = useCallback(() => setCount(count + 1), []);
NO6:Live Demo
useCallback
也可以接收一个空数组作为第二个参数,这样内联匿名函数就只会调用一次。这次代码实现了记忆功能,点击按钮会打印renderApp
,主要按钮的功能也正常,但是内部inc
按钮停止正常工作了。
计数器从0增加到1后就停止工作了。匿名函数被创建一次,调用了多次。因为匿名函数被创建的时候count是0,他的表现就像下面这样:
const inc = useCallback(() => setCount(1), []);
这个问题的根本原因在于,我们试图同时读写state
。幸运的是,react
提供了解决上面问题的方法:
useState with functional updates
const inc = useCallback(() => setCount(c => c + 1), []);
NO7:Live Demo
useState
返回的setters可以将函数作为参数,这个函数中可以读取上一state
的值。在这个例子中,Memoization正确工作,没有bug。
useReducer
const [count, dispatch] = useReducer(c => c + 1, 0);
NO8:Live Demo
useReducer memorization
和useState
中的例子一样正确运行了。因为dispatch
要保证渲染过程中参考对象一致,就不需要useCallback
了,dispatch
使得代码更少的犯Memoization相关错误。
useReducer vs useState
useReducer
更适合管理复杂对象或者下一state
依赖上一state
的情况。使用useReducer
的常见模式是和useContext
一起使用,避免在大型组件树中显示传递回调。
我比较推荐用useState
来管理组件内部数据,useReducer
适合管理需要在父子组件中传递的特殊双向数据。
总之,
React.memo
和useReducer
是最好的朋友;React.memo
和useState
是时而产生冲突的兄弟,会造成一些问题;谨慎使用useCallback
。
生活的一部分是工作,工作的一部分是解决问题取悦生活,所以好好生活,好好工作,好好热爱(●ˇ∀ˇ●)