在React应用中过度优化真的是一种噩梦。我们每天都要面对大量无用的优化,这些优化意在使代码变得“神秘”。一些开发人员甚至将 useMemo 和 useCallback 纳入他们的默认风格指南中,以简化事情。不要陷入这种迷思,因为 useMemo 甚至可能会减慢你的应用程序速度!记住,记忆化并不是免费的。
在这篇文章中,向你展示大多数开发人员如何过度使用 useMemo 钩子,并提供一些避免这种情况的技巧。当我第一次意识到我犯了这些错误时,我感到非常愚蠢。话不多说,开始吧!
为什么我们使用useMemo?
useMemo 是一个允许我们在组件重新渲染之间缓存计算结果的钩子。毫无疑问,它仅用于性能原因,并且应该与其他技术如 React.memo、useCallback、debouncing、并发渲染等一起使用。
尽管在某些情况下,这个钩子确实有帮助,并且发挥了重要作用,但大多数开发人员却错误地使用它。他们将每个变量都用 useMemo 包裹起来,希望随机的优化能带来成功。不出所料,这种方法只会导致代码可读性差和内存使用增加。
另一个需要记住的重要事项是,useMemo 只有在重新渲染阶段才有价值。在初始化期间,记忆化(memoization)会减慢应用程序的速度,并且这种影响会逐渐累积。这就是我所说的“记忆化并不是免费的”的意思。
什么时候它没有用甚至有害?
现在,看看下面的例子。这些都是我从一个项目中拿来的真实代码片段。你能说出哪个useMemo的使用实际上有意义吗?
export const NavTabs = ({ tabs, className, withExpander }) => {
const currentMainPath = useMemo(() => {
return pathname.split("/")[1];
}, [pathname]);
const isCurrentMainPath = useMemo(() => {
return currentMainPath === pathname.substr(1);
}, [pathname, currentMainPath]);
return (
<StyledWrapper>
<Span fontSize={18}>
{isCurrentMainPath ? (
t(currentMainPath)
) : (
<StyledLink to={`/${currentMainPath}`}>
{t(currentMainPath)}
</StyledLink>
)}
</Span>
</StyledWrapper>
);
};
通常我们在两种情况下使用 useMemo:记住一个引用并将其传递给 memo 组件,或缓存一些昂贵的计算。
现在想一想,我们在上面的例子中优化了什么?我们有原始值,并没有将任何更深层次的内容传递到组件树中,所以我们不需要保留引用。而且.split和===似乎不是需要记忆化的复杂计算。因此,我们可以轻松地在这个例子中去掉useMemo,节省一些文件空间 :)
export const Client = ({ clientId, ...otherProps }) => {
const tabs = useMemo(
() => [
{
label: t("client withdrawals"),
path: `/clients/${clientId}/withdrawals`
},
...
],
[t, clientId]
);
...
return (
<>
...
<NavTabs tabs={tabs} />
</>
)
}
export const NavTabs = ({ tabs, className, withExpander }) => {
return (
<Wrapper className={className} withExpander={withExpander}>
{tabs.map((tab) => (
<Item
key={tab.path}
to={tab.path}
withExpander={withExpander}
>
<StyledLabel>{tab.label}</StyledLabel>
</Item>
))}
</Wrapper>
);
};
在上面的例子中,我们将tabs变量记忆化,然后将其传递给 NavTabs。你认为优化tabs创建的主要意图是什么?这根本不是一个计算问题,所以实现它的人可能是想要保留引用并避免 NavTabs 的过度重新渲染。这在这里是正确的做法吗?
首先,NavTabs 是一个轻量级组件,可以多次重新渲染而不影响性能。其次,即使它是一个重量级组件,useMemo 也不会起作用。仅仅保留引用不足以防止 NavTabs 在每次Client组件重新渲染时重新渲染。为了优化性能,我们应该用React.memo包裹 NavTabs。
memo 函数返回我们组件的记忆化版本。只要它的 props 没有改变,这个版本通常不会在其父组件重新渲染时重新渲染。memo 使用浅比较props来决定组件是否应该更新。如果你有一些特定条件来决定组件何时应该重新渲染,你甚至可以指定自己的比较函数作为 memo 的第二个参数。
如何判断计算是否昂贵?
除非你在执行成千上万项复杂循环或计算阶乘,否则它可能不昂贵。你可以结合使用React Profiler和performance.now()来识别瓶颈,然后再应用优化技术。
避免在以下情况下使用 useMemo:
- 试图优化的计算很便宜。在这些情况下,使用useMemo的开销将超过其带来的好处。
- 不确定是否需要记忆化。最好先不使用它,然后在出现问题时逐步将优化引入代码。
- 记忆化的值没有传递到组件中。如果该值仅在JSX中使用,并且没有传递到组件树中,大多数情况下你可以避免优化。没有必要记住它的引用,因为它不会影响其他组件的重新渲染。
- 依赖数组变化太频繁。在这种情况下,useMemo不会带来任何性能优势,因为它每次都会重新计算。
如何正确使用的技巧
React 组件每次重新渲染时都会运行其主体,这发生在其道具或状态发生变化时。通常情况下,我们希望避免在渲染过程中进行昂贵的计算,因为它们会减慢组件的运行速度。
然而,大多数计算仍然非常快,因此很难理解哪些地方确实需要使用 useMemo。要开始有效且有目的性地进行优化,首先应找出问题所在。此外,不要忘记其他技术,例如:useCallback、React.memo、debouncing、代码拆分、lazy-loading 等。
我们看下面一个非常简单的例子。doCalculation 是一个人为放慢速度的函数,因此它需要 500ms 才能返回一个随机数。那么我们在这里遇到了什么问题呢?是的,每次值更新时,我们都要重新计算我们的重函数,这使得使用输入变得非常困难。
function doCalculation() {
const now = performance.now();
while (performance.now() - now < 500) {
// 停顿500毫秒
}
return Math.random();
}
export default function App() {
const [value, setValue] = useState(0);
const calculation = doCalculation();
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<h1>{`Calculation is ${calculation}`}</h1>
</div>
);
}
我们如何解决这个问题?只需用useMemo包裹doCalculation而不加任何依赖项。
const calculation = useMemo(() => doCalculation(), []);
现场实例:
https://codesandbox.io/s/happy-grass-0mjpbw?from-embed。