blog icon indicating copy to clipboard operation
blog copied to clipboard

React性能优化

Open aermin opened this issue 4 years ago • 0 comments

使用React.pureComponent,React.memo做性能优化

讲这两个api前需要提及React组件的更新,看这图:

image

如果shouldComponentUpdate 返回false,那就一定不用rerender(重新渲染 )更新这个组件了,不用执行调用render函数去返回React elements去做比对(vdoms diff),而且这个组件的子组件也不用去调用shouldComponentUpdate判断是否要更新了,因为父组件没更新(自己也没改变state)。但是如果shouldComponentUpdate 返回true,会进行组件的React elements比对,如果相同,则不用真实rerender这个组件,否则会rerender。

React.pureComponent

React.PureComponent,字面意思就是纯组件,可类比纯函数概念,所以当有一致的渲染输入(props和state)时,因为有同样的渲染效果,所以就不rerender了,以此优化性能。

React.Component是react的普通类组件,有个生命周期:在重新渲染之前应调用shouldComponentUpdate。在初始渲染之后,当接收到新的props或state时,在重新渲染之前应调用shouldComponentUpdate()。默认为true。

而React.PureComponent与React.Component之间的区别在于,React.Component虽然可以在shouldComponentUpdate()这个生命周期中进行自定义比较决定是否rerender,这样可以做性能优化。但是React.PureComponent的shouldComponentUpdate默认内置去浅比较prop和state来决定要不要rerender组件,解决了大量在Component虽然可以在shouldComponentUpdate写差不多模板代码的问题。

React.memo

React.pureComponent 服务于class组件,React.memo服务于函数组件,两者服务对象不同,功能相同。有点不同的是,React.memo只会浅比较props,以前函数式组件是stateless的,所以浅比较props也就够了,现依旧如此,useState hook带来的state改变和useContext hook带来的context改变,React.memo包(这货是个高阶组件)的组件仍会rerender。而且React.memo可以传第二个参数来实现类似shouldComponentUpdate的功能自定义决定组件是否rerender => React.memo(MyComponent, areEqual);。当然,return 值的意义两者反着来。

结论

也就是React.PureComponent与React.memo默认帮你用浅比较去决定要不要rerender,做性能优化。

当然,这边要注意不要mutable地去改动两者的props或者PureComponent的state,这样浅比较的结果为false,也就是这样你就算改了也不会rerender, 除非调用forceUpdate()强制更新。

可是,这样很多同学就会有事没事就React.PureComponent,React.memo一把梭,好像吃不用钱的蜜糖。但是,甲之蜜糖 乙之砒霜,并不是所有场合都适合使用这两个东东,用pureComponent会进行浅比较state和props,这个比较也是需要开销的,所以当你知道组件一定需要重新渲染时,就不要用pureComponent或者shouldComponentUpdate去做浅比较了,shouldComponentUpdate默认值为true,省了这个浅比较,直接去比较React元素有没有变,没有变就不更新,有变才更新。

所以啥时候需要使用这几个api进行优化呢?

以下内容来自React开发团队的回答

If we recommended that PureComponent be used everywhere, it would probably be the default already. Rather -- the comparison to decide whether or not a component should be rerendered costs something, and in the case that you do want to rerender, all of the time spent checking whether you should have rerendered is wasted.

Instead, we'd suggest you be conscious of where you need to do the comparisons. It's usually only in a couple of places in your app. Good candidates are on the children of a long list or around large parts of the app that change independently (that is, cases where you know the parent should often rerender but the child shouldn't). A few well-placed shouldComponentUpdate (or PureComponent) uses can go a long way.

大概是说: 如果我们建议在任何地方都使用PureComponent,那可能已经是默认设置了。然而,决定是否应重新渲染组件的比较会花费一些开销。在您确实要重新渲染的情况下,这会浪费了所有检查是否应该重新渲染的时间。那么建议通常只在您的应用程序中的几个地方,比如好的candidates会出现在列表较长的children中,或者在应用程序的大部分区域中都是独立变化的(也就是说,父节点经常rerender而子节点不应该跟着rerender的情况),这时一些适当的shouldComponentUpdate(或PureComponent)用法可以大有帮助。

旦总还专门发推 (是React核心开发者dan_abramov,不是朱一旦,手动狗头)

image

也可以看看sf的讨论

使用react hook: useCallback, useMemo做性能优化

这两个因为作用有点类似,都是用记忆计算结果来避免render的时候重新做一些计算,节省开销。不同的是,useMemo是记忆值,也就是返回值:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

而useCallback是记忆函数,也就是返回一个函数,

const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b],);

useCallback(fn, deps) 等同于 useMemo(() => fn, deps).

而且我原以为会缓存之前多次计算过的东西,没想到测试了下,只会缓存记忆上一次的,所以只有上一次和本次的依赖值相同时,才会读取上一次的计算结果,但是上一次之前的就算有跟本次的依赖值相同的,依旧要重新计算。如下图

image

useCallback 可以确保在重新渲染之间那个回调不会发生变化,除非依赖(第二个参数)改变

举个例子

image

图中RenderCallLogCallControl组件里面有一个onTransfer方法,如果不写useCallback,那么每次RenderCallLogCallControl组件rerender,也就是这个函数组件会被重新执行一次,因而onTransfer会跟着被重新创建,就算onTransfer还是一样的函数内容,但实际上引用地址不一样,那么EvSmallCallControl和其子组件TransferCallButton接收到onTransfer的onTransfer prop即有变动,该组件因此也会rerender。

这种情况并不想因为onTransfer被重复创建而rerender组件,性能浪费,那么可以用useCallback把这个onTransfer函数memo起来,让其不会变动(除非依赖transferRef改变), prop有onTransfer的组件也不会因此多做没必要的rerender。

reference

Deep dive with the React DevTools profiler Introducing the React Profiler Optimizing Performance React Top-Level API should-i-use-react-purecomponent-everywhere

aermin avatar Jun 18 '20 08:06 aermin