Redian新闻
>
请删掉99%的useMemo

请删掉99%的useMemo

科技

阿里妹导读


你的useMemo真正为你的项目带来了多少性能上的优化?由于useMemo和useCallback类似,所以本文全文会在大部分地方以useMemo为例,部分例子使用useCallback帮助大家更好的理解两个hooks。

不知道大家在什么情况下会考虑使用useMemo,你是不是这么想的?

「不知道行不行,但是感觉这里需要memo一下,用了指定能优化,就算不行也没啥影响」
「需要对数据处理,量好像还挺多,且不怎么需要变化,符合memo的能力」
「数据处理起来很麻烦,写方法不乐意,memo好像可以帮我套一层用方法的写法返回数据,真不戳」
useMemo能带来性能优化,但是你的useMemo真的为你的项目带来了多少性能上的优化?你确定你写的真的有带来优化,还是你的自我安慰?

你为什么要用useMemo?

我用了useMemo,减少了不必要的重渲染,应该是我能想到非常好的优化手段了。
我加了useMemo之后,就能够让我写的代码重渲染代价更小,太好了。
好好好,都这样想是吧?希望读完今天这篇文章能够让你的充满「自信」地删除你现在代码中95%的useMemo,然后你还会发现,项目可能反而运行的更快了,维护的成本更小了。

理解感悟阶段图

啥是useMemo?

从官方的文档中可以看到useMemo这个hooks的定义:它在每次重新渲染的时候能够缓存计算的结果。



官方文档定义
很多人了解useMemo,可能也就是这一句话,利用了useMemo能够缓存计算结果的特性。
useMemo再了解多一些会知道useMemo并不能帮助你提高组件第一次渲染的速度,只可能会在你重新渲染之后提高重渲染的速度(前提是你会正确使用useMemo)。
对于useMemo能够了解以上的信息,我觉得是处于「熟悉并使用了很久」阶段的同学,那么接下来我们再继续看一下官方文档中useMemo的用法有哪些:

1.跳过代价昂贵的重新计算

2.跳过组件的重渲染

3.记忆另一个Hook的依赖

4.记忆一个函数

核心源码

只挑重点,转换为白话,减少源码带来的恐惧感,请各位客官放心食用~
这里只看源码的重点部分,在重渲染时,useMemo会比较每一个依赖项,具体的比较参考Object.is(),虽然这个比较非常的快,但是这里想要给大家一个概念就是使用useMemo并不是百利而无一害,它也需要处理和比较。具体的后面我们会用例子来说明。
function areHookInputsEqual(  nextDeps: Array<mixed>,  prevDeps: Array<mixed> | null,): boolean {  // 省略部分  ...  // $FlowFixMe[incompatible-use] found when upgrading Flow  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {    // $FlowFixMe[incompatible-use] found when upgrading Flow    if (is(nextDeps[i], prevDeps[i])) {      continue;    }    return false;  }  return true;}
function is(x: any, y: any) {  return (    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare  );}


为什么一个组件会重渲染它自己?

众所周知:state或者props变化时,组件就会重渲染它自己



A 是 B 的充分条件,并不意味着 !A 是 !B 的充分条件
导致组件重渲染的还有一个可能性,那就是父组件重渲染
下面来看一段代码:
const Page = () => <Item />;
const App = () => { const [state, setState] = useState(1); return ( <div>
<button onClick={() => setState(state + 1)}> click to re-render {state} </button>
// Page是子组件,且没有props,里面也没有state <Page />
</div> );};
Page是一个没有props也没有state的组件,但是当我点击按钮时,App重渲染了(因为state变化),Page依旧重渲染,并且Page里面的Item也会重渲染,整个链路都会重渲染。如何打断这个重渲染?- React.memo
const Page = () => <Item />;const PageMemoized = React.memo(Page);
const App = () => { const [state, setState] = useState(1); return ( // ... same code as before <PageMemoized /> );};
当这些工作都做好了,此时,再去考虑你的Page的props,才是有意义的。
思考一下上面示例,我们可以得出结论,只有在唯一的一种场景下,缓存 props 才是有意义的:当组件的每一个 prop,以及组件本身被缓存的时候。
如果组件代码里有以下情形,我们可以毫无心理负担地删掉 useMemouseCallback
  • 它们被作为 attributes ,直接地或作为依赖树的上层,被传递到某个 DOM 上;

  • 它们被作为 props,直接地或作为依赖树的上层,被传递到某个未被缓存的组件上;

  • 它们被作为 props,直接地或作为依赖树的上层,被传递到某个组件上,而那个组件至少有一个 prop 未被缓存;

避免每次渲染时进行昂贵的计算

这里暂时使用这篇文章计算的数据:https://www.developerway.com/posts/how-to-use-memo-use-callback
计算代码:https://codesandbox.io/s/measure-without-memo-tnhggk?file=/src/page.tsx
读到这边,想必读者也应该知道useMemo到底是做啥的,正如这一个小标题所说的——useMemo的主要目标是避免每次渲染时进行昂贵的计算。那什么是昂贵的计算
不知道,官网好像没写,或者说你没找到。所以你就不管他三七二十一,用就完事了。创建新日期?过滤、映射或排序数组?创建一个对象?全部useMemo一把梭,useMemo终将占领所有的React项目!
好吧,那我们拿数据来看看,比如说我这有一系列的国家和地区(250个),你希望对它进行排序,然后展示出来。
const Item = ({ country }: { country: Country }) => {  return <button>{country.name}</button>;};

const List = ({ countries }) => { // sorting list of countries here const sortedCountries = orderBy(countries, 'name', sort);
return ( <> {sortedCountries.map((country) => ( <Item country={country} key={country.id} /> ))} </> );};

渲然后的按钮列表

在没有memo的情况下,将整个CPU速度降低6倍,对包含250个数据进行排序只需要不到2毫秒,相比之下,渲染整个列表(只是button带文字),需要超过20毫秒
日常开发来说,我们很少有这么多数据的处理。再者我们只渲染了普通的Button
所以,你真正要做的是memo这个数组的操作,还是说memo组件的渲染和更新呢?
const List = ({ countries }) => {  const content = useMemo(() => {    const sortedCountries = orderBy(countries, 'name', sort);
return sortedCountries.map((country) => <Item country={country} key={country.id} />); }, [countries, sort]);
return content;};
当我们memo了组件之后我们发现整体的的渲染列表时间从原先的20毫秒,减少了不到2毫秒(18毫秒左右)
在实际场景中,数组往往比示例中的更小,同时渲染的内容比示例中的更复杂,因此更慢。所以总的来说「计算」与「渲染」之间的耗时往往超过 10 倍
问题又来了,那为啥一定要删掉它们呢?把所有东西缓存起来不是更好吗?哪怕只让重渲染速度提升了 2ms,这里提升 2ms,那里提升 2ms,累加起来就很可观了呀。换个角度看,如果完全不写 useMemo,那么应用就会在这里慢 2ms,在那里又慢 2ms,很快我们的应用就会比它们原本能达到的程度慢的多了。
听起来很有道理 ,并且,如果不是考虑到另一点的话,以上推论确实 100% 说得通。这一点便是:缓存并不是毫无开销的。如果我们使用 useMemo,在初始渲染过程中 React 就需要缓存其值了——这当然也产生耗时。没错,这耗时很微小,在我们的应用中,缓存上述提到过的排序国家列表耗时不超过 1ms。但是!这才会产生货真价实的叠加效应!在初始渲染让你的应用第一次呈现在屏幕前的过程中,当前页面的每一个元素都会经历这一过程,这将导致 10~20 ms,或更糟糕的,接近 100ms 的不必要的延时。
与初始渲染相比,重渲染仅仅在页面某些部分改变时发生。在一个架构合理的应用中,只有这些特定区域的组件才会重渲染,而非整个应用(页面)。那么在一次寻常的重渲染中,总的“计算”的消耗和我们上面提到的例子(注:指 250 个元素的排序列表)相比,会高出多少呢?2~3 倍?,就假设有 5 倍好了,那也仅仅是节省了 10ms 的渲染时间,这么短的时间间隔我们的肉眼是无法察觉的,并且在十倍的渲染时间下,这 10 ms 也确实很不起眼。可作为代价的是,它确实拖慢了每次都会发生的初始渲染过程😔。

常见的错误用法(重点)


初级

这里的useCallback毫无用处,当Component重渲染,所有相关的子组件全部都会重渲染(无视props),在这个情形下,对于clickmemo将毫无意义。
const Component = () => {  const onClick = useCallback(() => {    /* do something */  }, []);  return <button onClick={onClick}>Click me</button>};
此时你的子组件被memo包裹,onClick也被useCallback包裹,但是value并没有被包裹,这个时候,你的Component重渲染,你的MemoItem依旧会重渲染,此时useCallback还是什么都没做。
const Item = () => <div> ... </div>const MemoItem = React.memo(Item)const Component = () => {  const onClick = useCallback(() => {    /* do something */  }, []);  return <MemoItem onClick={onClick} value={[1,2,3]}/>};


中级

这个看起来应该没啥问题了吧?onClickuseCallback包裹了,然后MemoItem也被memo了,这回天王老子来了都不能重渲染吧,不然我学的知识都白学了?
const Item = () => <div> ... </div>const MemoItem = React.memo(Item)const Component = () => {  const onClick = useCallback(() => {    /* do something */  }, []);  return   <MemoItem onClick={onClick}>    <div>something</div>  </MemoItem>};
没错,这个会重渲染哦。上面这段代码相当于:
// 以下写法均等价,也就是说在props中传递children,和直接children嵌套是一致的React.createElement('div',{  children:'Hello World'})
React.createElement('div',null,'Hello World')
<div>Hello World</div>
const Item = () => <div> ... </div>const MemoItem = React.memo(Item) // uselessconst Component = () => {  const onClick = useCallback(() => { //useless    /* do something */  }, []);  return   <MemoItem     onClick={onClick}     children={<div>something</div>}  />};
有些同学看到这里还不理解:“就算你说子组件相当于children,但是我div还是一模一样的,你凭啥说我props变化了”。有这种想法的同学先放一放,我们看最后一个。


高级

好好好,你要这样说是吧,行,那我都包裹起来。这回玉皇大帝来了也拦不住我,这回必memo住了!
const Item = () => <div> ... </div>const Child = () => <div>sth</div>
const MemoItem = React.memo(Item)const MemoChild = React.memo(Child)
const Component = () => { const onClick = useCallback(() => { /* do something */ }, []); return ( <MemoItem onClick={onClick}> <MemoChild /> </MemoItem> )};
答案还是没有memo住,为什么呢?来我们把MemoChild单独拿出来解析一下,它是怎么执行的:
const child = <MemoChild />;
const child = React.createElement(MemoChild,props,childen);
const child = {  type: MemoChild,  props: {}, // same props  ... // same interval react stuff}
前面的问题也迎刃而解,其实每次create的时候,创建的child都是不一样的对象,所以一比较就重渲染了。


终极解决思路

如果你真的想要memo住,你应该memo的目标是Element本身,而不是ComponentuseMemo会缓存之前的值,如果memo的依赖项没有变化,则会用缓存的数据返回。
const Child = () => <div>sth</div>
const MemoItem = React.memo(Item)
const Component = () => { const onClick = useCallback(() => { /* do something */ }, []); const child = useMemo(()=> <Child /> ,[]) return ( <MemoItem onClick={onClick}> {child} </MemoItem> )};
终于,我们的组件memo成功了!
如果你觉得自己之前完全不知道这个特性,不需要沮丧,React-Query作者Dominik很长一段时间也不知道这个特性。对于这一块可以展开说很多知识点,涵盖了JSX本质,react本身的diff,这里不再展开赘述,感兴趣的可以查看这篇文档:
《One simple trick to optimize React re-renders》https://kentcdodds.com/blog/optimize-react-re-renders
anyway,成功来之不易,现在还觉得useMemo好用吗?你现在辛辛苦苦打下的江山,下一个人过来只需要随手传递一些东西作为props,我们又回到了最初的起点。

你应该在所有地方加上useMemo吗?

一般来说,如果是基础的中后台应用,大多数交互都比较粗糙,通常不需要。如果你的应用类似图形编辑器,大多数交互是颗粒状的(比如说移动形状),那么此时useMemo可能会起到很大的帮助。
使用 useMemo 进行优化仅在少数情况下有价值:
  • 你明确知道这个计算非常的昂贵,而且它的依赖关系很少改变。

  • 如果当前的计算结果将作为memo包裹组件的props传递。计算结果没有改变,可以利用useMemo缓存结果,跳过重渲染。

  • 当前计算的结果作为某些hook的依赖项。比如其他的useMemo/useEffect依赖当前的计算结果。

这几句是不是很熟悉,就是开头我说的useMemo的官方文档的用法中提到的这几项。
在其他情况下,将计算过程包装在 useMemo 中没有任何好处。不过这样做也没有重大危害,所以一些团队选择不考虑具体情况,尽可能多地使用 useMemo,这种做法会降低代码可读性。此外,并不是所有 useMemo 的使用都是有效的:一个“永远是新的”的单一值就足以破坏整个组件的记忆化效果。

没了useMemo,我不知道怎么办了


例子

这是一个存在严重渲染性能问题的组件,ExpensiveTree是一个渲染极其昂贵的组件:
import { useState } from 'react'; export default function App() {  let [color, setColor] = useState('red');  return (    <div>      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p style={{ color }}>Hello, world!</p>      <ExpensiveTree />    </div>  );} function ExpensiveTree() {  let now = performance.now();  while (performance.now() - now < 100) {    // Artificial delay -- do nothing for 100ms  }  return <p>I am a very slow component tree.</p>;}
try it:https://codesandbox.io/s/frosty-glade-m33km?file=/src/App.js:23-513
color改变的时候,ExpensiveTree也会重渲染,而ExpensiveTree的渲染非常的昂贵。
经过我们前面的学习,我们知道,这里适合用useMemo来解决,因为它确实是昂贵的计算,并且我确实感觉到了卡顿和缓慢,影响了我的项目正常渲染。
但是真的一定要用useMemo吗?

解决方案1:状态下移

如果你仔细的看这段代码,你会发现,返回的结果中只有部分与color关联:
export default function App() {  let [color, setColor] = useState('red');  return (    <div>      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p style={{ color }}>Hello, world!</p>      <ExpensiveTree />    </div>  );}
所以我们可以将该部分提取出来,并将状态下移到其他组件中:
export default function App() {  return (    <>      <Form />      <ExpensiveTree />    </>  );} function Form() {  let [color, setColor] = useState('red');  return (    <>      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p style={{ color }}>Hello, world!</p>    </>  );}
至此,color改变,只有Form会重渲染,问题解决了!
try it:https://codesandbox.io/s/billowing-wood-1tq2u?file=/src/App.js:64-380

解决方案2:内容提升

如果说我们在div的最外层也用到了color,此时解决方案1就失效了:
export default function App() {  let [color, setColor] = useState('red');  return (    <div style={{ color }}>      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p>Hello, world!</p>      <ExpensiveTree />    </div>  );}
完了,这回还咋提取啊?最外层父级<div>还得用color呢,这只能用memo了吧?
export default function App() {  return (    <ColorPicker>      <p>Hello, world!</p>      <ExpensiveTree />    </ColorPicker>  );} function ColorPicker({ children }) {  let [color, setColor] = useState("red");  return (    <div style={{ color }}>      <input value={color} onChange={(e) => setColor(e.target.value)} />      {children}    </div>  );}
try it :https://codesandbox.io/s/wonderful-banach-tyfr1?file=/src/App.js:58-423
我们将程序一分为二,依赖颜色的部分以及变量color本身已经都放在ColorPicker中了。
不依赖color的部分保留在App中,并作为ColorPickerchildren
color改变,colorPicker重渲染,但是childrenprops并没有变化,因此React会复用之前的childrenExpensiveTree没有重渲染,问题解决!

总结

在用useMemomemo等优化方案之前,看看是否可以将变化的部分与不受影响的部分分开,可能是更有意义的。
使用拆分的方法有趣的是,我们并不会借助到任何的性能工具,而拆分本质也与性能无关。使用children也能遵循从上到下的数据流,并减少通过树向下查找的属性数量。在这种情况下,提高性能只是锦上添花,而不是最终目标,真正意义上做到一举两得。

为什么一定要移除?

图源自:Dominik【ReactJs • TypeScript • Father of two】

有些人可能说,我就是喜欢useMemouseCallback,为啥要我移除,我只要捋清楚前面说的逻辑,让我的useMemo真正派上用场就好了!
技术上来说,是的,你可以。
但是你到现在都没发现useMemouseCallback使用的有问题,那么说明你现在正在写的程序并没有性能问题。
如果你坚持一定要用,好的,你理解了使用规则,非常完美的将你的程序用memo包裹起来,密不透风。并且你告诫自己,以后开发/增加需求的时候,一定要注意不要破坏掉整个memo的链路,你小心翼翼。那请问你能否保证与你一起合作同学在开发时也能注意到这一点?你能否保证项目交付给下一任同学时,他/她能够坚持你的维护之道?

React团队的看法

原视频链接:https://www.youtube.com/watch?v=lGEMwh32soc&t=620s
React团队也发现了,如果我们不用memo,可能会导致部分性能问题。但是如果我们要用memo,又要有很强的心智负担,需要考虑多个依赖关系能被正确的使用和包裹。

颜色选择器优化
如果说,有一个东西能将你要做的所有全部都正确的memo住,岂不妙哉?



自动记忆
代号:React Forget 正在研究中,这是一个可以帮助你自动memo的编译器,你们对于自动memo的问题,他们也正在解决当中。

React Forget

最后

最后我们再一起看一下前面提到的几个想法,你们现在会怎么考虑这几种case:
  • 「不知道行不行,但是感觉这里需要memo一下,用了指定能优化,就算不行也没啥影响」

  • 「需要对数据处理,量好像还挺多,且不怎么需要变化,符合memo的能力」

  • 「数据处理起来很麻烦,写方法不乐意,memo好像可以帮我套一层用方法的写法返回数据,真不戳」

我的看法是,如果你发现你的项目并没有明显的迟钝或者卡顿的现象,不要使用;不要期待你现在写的memo能为项目带来长远的收益,因为它太容易被破坏了,一旦有新的项目维护同学不懂memo,就容易将整个memo链路打破;如果确实有卡顿的现象,请合理使用memo的记忆化功能(参考常见的错误用法),帮助优化卡顿或者迟缓现象。

参考文章:

这些文章都非常优秀,可以帮助您更好的了解useMemo的正确使用

https://react.dev/reference/react/useMemo

https://react.dev/reference/react/memo#memo

https://tkdodo.eu/blog/the-uphill-battle-of-memoization

https://www.developerway.com/posts/how-to-use-memo-use-callback

https://overreacted.io/before-you-memo/

https://kentcdodds.com/blog/what-is-jsx

https://kentcdodds.com/blog/optimize-react-re-renders

https://juejin.cn/post/7251802404877893689#heading-2

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
China Restricts Use of AI in Scientific Research[电脑] 一只蓝色的仓鼠——vaxee AX Wireless Mouse每天10句英语口语|Just a moment中国学生扎堆!圈粉无数的UIUC究竟有何魅力?The Contemporary Artist Using Crosses to Push Boundaries[歪解] sense of humor暴风雪预警&Snow Emergency:本周二波士顿地区预计有20-30cm降雪!Snow Emergency今晚10点生效!【USD$1999/人起 三餐全包!】激情狂野肯尼亚东非大裂谷!动物大迁徙百万大军“天堂之渡”又降了!SSENSE 低至4折,终于对Essentials卫衣下手了!Java 近期新闻:JDK 22 的 JEP、Spring Shell、Quarkus、Apache Camel、JDKMon没有美国求职经历的UIUC 学员,秋招是如何成功斩获了美国知名会计师事务所的offer?大球中的U2面对压力,老胡删掉了自己刚说的人话【买房】Melrose|8B6B|标价$1,599,990年薪$118K,SEI Investments (US) 2024 Summer Analyst申请中!PLUS福利购丨1月23日,Swisse健康年货满399元减60元!50元E卡不限量送,PLUS会员满件加赠PLUS年卡!最好的我们(古詩英譯) 歸家 – 杜牧初冬的小阳春---APEC会议上的元首的互动西雅图“THE EMERALD”美丽岸高端海景公寓---Penthouse高端户型鉴赏网红“男妲己”道歉,暂停工作:将删掉视频!这个5A景区也致歉了兴旺超市年终特价!红梅糯米$5.99!李锦记鲍鱼2/$8.98!日本番薯$0.99!炒牛肉$6.99!游水鲈鱼$6.99!糟了!这所计算机牛校将CS转学名额砍掉90%...100元起拍的“18611999999”,2893次出价,以2614万元成交!评论区炸了…基于腾讯云 Lighthouse 自建 Grafana 和 Prometheus 服务OceanBase数据库炸场!具备OLTP完整核心功能,实验室版本不输ClickHouseSimons折扣区2.5折起!Helly Hansen羽绒大衣 99.95元!又降了!SSENSE 低至4折,购物车里的Essentials卫衣终于可以买了!小红书电商发布年度榜单;珀莱雅CMO叶伟离职;lululemon合作杨紫琼发布龙年短片... | 刀法品牌热讯18611999999,拍出超2614万元兴旺超市周末特价!辛拉面$7.99!长糯米$2.99!高丽菜$0.39!金沙骨$2.99!游水侧鱼$3.99!免费无门槛,被MIT力荐的USACO计算机竞赛,2023-2024赛季安排正式公布啦!回国的零食,小吃(多图)9999999999999999.0 - 9999999999999998.0旧金山拜、习会双方的得失
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。