React Hooks 性能优化的正确姿势 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liumingyi1
V2EX    React

React Hooks 性能优化的正确姿势

  •  1
     
  •   liumingyi1 2020-11-12 09:46:30 +08:00 7133 次点击
    这是一个创建于 1794 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    React Hooks 出来很长一段时间了,相信有不少的朋友已经深度使用了。无论是 React 本身,还是其生态中,都在摸索着进步。鉴于我使用的 React 的经验,给大家分享一下。对于 React hooks,性能优化可以从以下几个方面着手考虑。

    场景 1

    在使用了 React Hooks 后,很多人都会抱怨渲染的次数变多了。没错,官方就是这么推荐的:

    我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。

    function Box() { const [position, setPosition] = useState({ left: 0, top: 0 }); const [size, setSize] = useState({ width: 100, height: 100 }); // ... } 

    这种写法在同时 setPositionsetSize 的时候,相较于 class 写法会额外多出一次 render。这也就是渲染次数变多的根本原因。当然这种写法仍然值得推荐,可读性和可维护性更高,能更好的逻辑分离。

    针对这种场景若出现十几或几十个 useState 的时候,可读性就会变差,这个时候就需要相关性的组件化了。以逻辑为导向,抽离在不同的文件中,借助 React.memo 来屏蔽其他 state 导致的 rerender

    const Position = React.memo(({ position }: PositionProps) => { // position 相关逻辑 return ( <div>{position.left}</div> ); }); 

    因此在 React hooks 组件中尽量不要写流水线代码,保持在 200 行左右最佳,通过组件化降低耦合和复杂度,还能优化一定的性能。

    场景 2

    class 对比 hooks,上代码:

    class Counter extends React.Component { state = { count: 0, }; increment = () => { this.setState((prev) => ({ count: prev.count + 1, })); }; render() { const { count } = this.state; return <ChildComponent count={count} OnClick={this.increment} />; } } 
    function Counter() { const [count, setCount] = React.useState(0); function increment() { setCount((n) => n + 1); } return <ChildComponent count={count} OnClick={increment} />; } 

    凭直观感受,你是否会觉得 hooks 等同于 class 的写法?错,hooks 的写法已经埋了一个坑。在 count 状态更新的时候, Counter 组件会重新执行,这个时候会重新创建一个新的函数 increment。这样传递给 ChildComponentonClick 每次都是一个新的函数,从而导致 ChildComponent 组件的 React.memo 失效。

    解决办法:

    function usePersistFn<T extends (...args: any[]) => any>(fn: T) { const ref = React.useRef<Function>(() => { throw new Error('Cannot call function while rendering.'); }); ref.current = fn; return React.useCallback(ref.current as T, [ref]); } 
    // 建议使用 `usePersistFn` const increment = usePersistFn(() => { setCount((n) => n + 1); }); // 或者使用 useCallback const increment = React.useCallback(() => { setCount((n) => n + 1); }, []); 

    上面声明了 usePersistFn 自定义 hook,可以保证函数地址在本组件中永远不会变化。完美解决 useCallback 依赖值变化而重新生成新函数的问题,逻辑量大的组件强烈建议使用。

    不仅仅是函数,比如每次 render 所创建的新对象,传递给子组件都会有此类问题。尽量不在组件的参数上传递因 render 而创建的对象,比如 style={{ width: 0 }} 此类的代码用 React.useMemo 来优化。

    const CustomCompOnent= React.memo(({ width }: CustomComponentProps) => { const style = React.useMemo(() => ({ width } as React.CSSProperties), [width]); return <ChildComponent style={style} />; }); 

    style 若不需改变,可以提取到组件外面声明。尽管这样做写法感觉太繁琐,但是不依赖 React.memo 重新实现的情况下,是优化性能的有效手段。

    场景 3

    对于复杂的场景,使用 useWhyDidYouUpdate hook 来调试当前的可变变量引起的 rerender。这个函数也可直接使用 ahooks 中的实现。

    function useWhyDidYouUpdate(name, props) { const previousProps = useRef(); useEffect(() => { if (previousProps.current) { const allKeys = Object.keys({ ...previousProps.current, ...props }); const changesObj = {}; allKeys.forEach(key => { if (previousProps.current[key] !== props[key]) { changesObj[key] = { from: previousProps.current[key], to: props[key] }; } }); if (Object.keys(changesObj).length) { console.log('[why-did-you-update]', name, changesObj); } } previousProps.current = props; }); } const Counter = React.memo(props => { useWhyDidYouUpdate('Counter', props); return <div style={props.style}>{props.count}</div>; }); 

    useWhyDidYouUpdate 中所监听的 props 发生了变化,则会打印对应的值对比,是调试中的神器,极力推荐。

    场景 4

    借助 Chrome Performance 代码进行调试,录制一段操作,在 Timings 选项卡中分析耗时最长逻辑在什么地方,会展现出组件的层级栈,然后精准优化。

    场景 5

    React 中是极力推荐函数式编程,可以让数据不可变性作为我们优化的手段。我在 React class 时代大量使用了 immutable.js 结合 redux 来搭建业务,与 ReactPureComponnet 完美配合,性能保持非常好。但是在 React hooks 中再结合 typescript 它就显得有点格格不入了,类型支持得不是很完美。这里可以尝试一下 immer.js,引入成本小,写法也简洁了不少。

    const nextState = produce(currentState, (draft) => { draft.p.x.push(2); }) // true currentState === nextState; 

    场景 6

    复杂场景使用 Map 对象代替数组操作,map.get(), map.has(),与数组查找相比尤其高效。

    // Map const map = new Map([['a', { id: 'a' }], ['b', { id: 'b' }], ['c', { id: 'c' }]]); // 查找值 map.has('a'); // 获取值 map.get('a'); // 遍历 map.forEach(n => n); // 它可以很容易转换为数组 Array.from(map.values()); // 数组 const list = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]; // 查找值 list.some(n => n.id === 'a'); // 获取值 list.find(n => n.id === 'a'); // 遍历 list.forEach(n => n); 

    结语

    React 性能调优,除了阻止 rerender,还有与写代码的方式有关系。最后,我要推一下近期写的 React 状态管理库 https://github.com/MinJieLiu/heo,也可以作为性能优化的一个手段,希望大家从 redux 的繁琐中解放出来,省下的时间用来享受生活

    26 条回复    2021-01-12 14:03:05 +08:00
    answeryou
        1
    answeryou  
       2020-11-12 10:38:43 +08:00
    > 这种写法在同时 setPosition 和 setSize 的时候,相较于 class 写法会额外多出一次 render

    两次 render? 同时赋值只有一次啊
    cityboy
        2
    cityboy  
       2020-11-12 10:44:33 +08:00
    大佬问个题外话,react hooks 在实战中用什么共享全局变量啊,useContext 感觉不是很好用啊
    ruoxie
        3
    ruoxie  
       2020-11-12 10:46:08 +08:00
    @cityboy 我们在用 hox,简单易用
    KuroNekoFan
        4
    KuroNekoFan  
       2020-11-12 10:56:49 +08:00
    其实 depth 不要用 object,用基本类型或者自定义的序列化函数把 object 序列化成 string/number 就可以解决 object 作为依赖导致 unexceptable 的变化,useEffect 和 useCallback 都可以这样处理
    liumingyi1
        5
    liumingyi1  
    OP
       2020-11-12 11:01:56 +08:00
    @answeryou 因为是两个 useState
    liumingyi1
        6
    liumingyi1  
    OP
       2020-11-12 11:11:58 +08:00
    @cityboy 你可以试试 https://github.com/MinJieLiu/heo,hox 也行,不过 hox 好像是单例的
    leelz
        7
    leelz  
       2020-11-12 11:16:15 +08:00
    React.memo 是一个 HOC,如果不足以抽成组件的那块也用 React.memo 来 wrapper,反而会适得其反。
    answeryou
        8
    answeryou  
       2020-11-12 11:20:55 +08:00
    @liumingyi1 两个 useState 同时设置值也只会一次 re-render 啊
    liumingyi1
        9
    liumingyi1  
    OP
       2020-11-12 11:48:22 +08:00
    @leelz 并不算真正意义上的 HOC 吧,只是多了层对比,影响微乎其微
    liumingyi1
        10
    liumingyi1  
    OP
       2020-11-12 12:36:00 +08:00
    @answeryou 这里的确是我写错了,应该要加上异步的环境下
    liumingyi1
        11
    liumingyi1  
    OP
       2020-11-12 12:43:45 +08:00
    场景 1:这种写法在异步的条件下,比如调用接口返回后,同时 `setPosition` 和 `setSize`,相较于 class 写法会额外多出一次 `render`。

    文章的失误敬请谅解
    nuanyang
        13
    nuanyang  
       2020-11-12 14:34:37 +08:00
    关于 useCallback 和 useMemo 的使用需要小心点避免滥用,额外的 deps 比对的成本以及额外的控件成本反而会对性能是一种损耗。
    https://jancat.github.io/post/2019/translation-usememo-and-usecallback/
    https://blog.logrocket.com/rethinking-hooks-memoization/
    liumingyi1
        14
    liumingyi1  
    OP
       2020-11-12 15:00:33 +08:00
    性能优化的确是把双刃剑
    answeryou
        15
    answeryou  
       2020-11-12 15:53:44 +08:00
    @liumingyi1 异步条件下, 不论是 Hook 还是 Class 其实表现都是一样的, 都会多一次 render
    liumingyi1
        16
    liumingyi1  
    OP
       2020-11-12 16:47:21 +08:00
    @answeryou 主要是 class 下,setState 一般都在对象里
    peterjose
        17
    peterjose  
       2020-11-12 17:42:28 +08:00
    @KuroNekoFan 序列化性能差到负优化。。。。
    peterjose
        18
    peterjose  
       2020-11-12 17:44:50 +08:00
    @nuanyang 对的 切忌提前优化
    gouflv
        19
    gouflv  
       2020-11-12 18:41:35 +08:00 via iPhone
    很实用的总结。每次写 hooks 都会怀疑人生
    yikyo
        20
    yikyo  
       2020-11-12 18:54:12 +08:00
    hooks 没用好是个灾难。
    KuroNekoFan
        21
    KuroNekoFan  
       2020-11-12 19:59:25 +08:00
    @peterjose 这看具体实现吧,如果随便 json.stringify 然后对比一下那自然不太行,高效的实现自然还是要看具体的场景了
    nullEDYT
        22
    nullEDYT  
       2020-11-17 12:07:27 +08:00
    大佬们,请问场景 2 中的 ‘可以保证函数地址在本组件中永远不会变化’是指什么不会变化
    funnyecho
        23
    funnyecho  
       2020-11-17 14:18:49 +08:00
    @nullEDYT usePersistFn() 返回的函数永远是同一个高阶函数(也就是同一个对象,通过 useRef.current 来保证 render 时的引用一致)
    shuding
        24
    shuding  
       2020-11-18 21:49:56 +08:00
    挺不错的总结。多个 state 又涉及到同时更新的情况,其实挺适合用 `useReducer`。`memo` 只能避免子组件的重渲染,而且有 `children` 的话就没有意义了。
    huijiewei
        25
    huijiewei  
       2020-11-28 09:02:42 +08:00 via iPhone
    想来想去 你这 hooks 写法还是按照 class 的思路来的

    写法就不对
    liumingyi1
        26
    liumingyi1  
    OP
       2021-01-12 14:03:05 +08:00
    @huijiewei 说说你的思路
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     880 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 33ms UTC 21:42 PVG 05:42 LAX 14:42 JFK 17:42
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86