在React应用中过度优化真的是一种噩梦。我们每天都要面对大量无用的优化,这些优化意在使代码变得“神秘”。一些开发人员甚至将 useMemo 和 useCallback 纳入他们的默认风格指南中,以简化事情。不要陷入这种迷思,因为 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:
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。
本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-92175-0.html现在停止滥用useMemo吧!
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com