• 如何做 React 性能优化?


    大家好,我是前端西瓜哥。今天带大家来学习如何做 React 性能优化。

    使用 React.memo()

    一个组件可以通过 React.memo 方法得到一个添加了缓存功能的新组件。

    const Comp = props => {
      //
    }
    
    const MemorizedComp = React.memo(Comp);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    再次渲染时,如果 props 没有发生改变,就跳过该组件的重渲染,以实现性能优化。

    这里的 关键在于 props 不能改变,这也是最恶心的地方

    对于像是字符串、数值这些基本类型,对比没有问题。但对于对象类型,就要做一些缓存工作,让原本没有改变的对象或函数仍旧指向同一个内存对象。

    因为每次函数组件被执行时,里面声明的函数都是一个全新的函数,和原来的函数指向不同的内存空间,全等比较结果是 false。

    关于 React.memo 的具体使用,可以看看我的这篇文章:《React.memo 如何使用?

    处理 props 比较问题

    React.memo() 最疼痛的就是处理 props 比较问题。

    我们看个例子:

    const MemorizedSon = React.memo(({ onClick }) => {
      // ...
    })
    
    const Parent() {
      // ...
      const onClick = useCallback(() => {
        // 一些复杂的判断和逻辑
      }, [a, setA, b, onSava]);
      return (
        
    ) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面为了让函数的引用不变,使用了 useCallback。函数里用到了一些变量,因为函数组件有闭包陷阱,可能会导致指向旧状态问题,所以需要判断这些变量是否变化,来决定是否使用缓存函数。

    这里就出现了一个 连锁反应,就是我还要给变量中的对象类型做缓存,比如这里的 setA 和 onSave 函数。然后这些函数可以又依赖其他函数,一直连锁下去,然后你发现有些函数甚至来自其他组件,通过 props 注入。

    啊我真的是麻了呀,我优雅的 React 一下变得丑陋不堪。

    怎么办,一个方式是用 ref。ref 没有闭包问题,且能够在组件每次更新后保持原来的指向。

    const MemorizedSon = React.memo(({ onClickRef }) => {
      const onClick = onClickRef.current;
    })
    
    const Parent() {
      // ...
      const onClick = () => {
        // 一些复杂的判断和逻辑
      };
      
      const onClickRef = useRef(onClick);
      onClickRef.current = onClick;
      
      return (
        
    ) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    或者

    const MemorizedSon = React.memo(({ onClick }) => {
      // ...
    })
    
    const Parent() {
      // ...
      const onClick = useCallback(() => {
        const {a, b} = propsRef.current;
        const {setA, setSave} = stateRef.current;
        // 一些复杂的判断和逻辑
      }, []);
      return (
        
    ) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    当然官方也注意到这种场景,提出了 useEvent 的提案,希望能尽快实装吧。

    function Chat() {
      const [text, setText] = useState('');
    
      const onClick = useEvent(() => {
        sendMessage(text);
      });
      return ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    The code inside useEvent “sees” the props/state values at the time of the call. The returned function has a stable identity even if the props/state it references change. There is no dependency array.

    用了 useEvent 后,指向是稳定的,不需要加依赖项数组。

    提案详情具体看下面这个链接:

    https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md

    跳过中间组件

    假设我们的组件嵌套是这样的:A -> B -> C。

    其中 C 需要拿到 A 的一个状态。B 虽然不需要用到 A 的任何状态,但为了让 C 拿到状态,所以也用 props 接收了这个,然后再传给 C。

    这样的话,A 更新状态时,B 也要进行不必要的重渲染。

    对于这种情况,我们可以让中间组件 B 跳过渲染:

    1. 给 B 应用 React.memo,A 的状态不再传给 B;

    2. A 的状态通过发布订阅的方式传给 C(比如 useContext,或通过状态管理)

    状态下放

    假设同样还是 A -> B -> C 形式的组件嵌套。

    C 需要来自 A 的状态,B 会帮忙通过 props 传递状态过来。A 状态更新时,A、B、C 都会重渲染。

    如果状态只有 C 一个组件会用到,我们可以考虑直接把状态下放到 C。这样当状态更新时,就只会渲染 C。

    组件提升

    将组件提升到父组件的 props 上。

    export default function App() {
      return (
        
          

    Hello, world!

    ); } function ColorPicker({ children }) { let [color, setColor] = useState("red"); return (
    { color }}> setColor(e.target.value)} /> {children}
    ); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里 ColorPicker 更新 color 状态后,因为 ExpensiveTree 来自外部 props,不会改变,不会重渲染。除非是 App 中发生了状态改变。

    正确使用列表 key

    进行列表渲染时,React 会要求你给它们提供 key,让 React 识别更新后的位置变化,避免一些不必要的组件树销毁和重建工作。

    比如你的第一个元素是 div,更新后发生了位置变化,第一个变成了 p。如果你不通过 key 告知新位置,React 就会将 div 下的整棵树销毁,然后构建 p 下的整棵树,非常耗费性能。

    如果你提供了位置,React 就会做真实 DOM 的位置移动,然后做树的更新,而不是销毁和重建。

    如果你想深入了解 key,可以看我写的这篇文章:《React 中的列表渲染为什么要加 key ?

    注意状态管理库的触发更新机制

    对于使用 Redux 的进行状态管理的朋友来说,我们会在函数组件中通过 useSelector 来订阅状态的变化,自动更新组件。

    const Comp() {
      const count = useSelector(state => state.count);
    }
    
    
    • 1
    • 2
    • 3
    • 4

    useSelector 做了什么事?它会订阅 state 的变化,当 state 变化时,会运行回调函数得到返回值,和上一次的返回值进行全等比较。如果相等,不更新组件;如果不等,更新组件。然后缓存这次的返回值。

    上面这种情况还好,我们再看看写成对象的形式

    import { shallowEqual, useSelector } from 'react-redux'
    
    const Comp() {
      const { count, username } = useSelector(state => ({
        count: state.count,
        username: state.username
      }), shallowEqual);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面这种写法,因为默认用的是全等比较,所以每次 state 更新后比较结果都是 false,组件每次都更新。对于组合成的对象,你要用 shallowEqual 浅比较来替代全等比较,避免不必要的更新

    有一种情况比较特别,假设 state.userInfo 有多个属性,username、age、acount、score、level 等。有些人会这样写:

    const Comp() {
      const { username, age } = useSelector(state => state.userInfo), shallowEqual);
    }
    
    
    • 1
    • 2
    • 3
    • 4

    看起来没什么问题,但里面是有陷阱的:虽然我们的组件只用到 username 和 age,但 useSelector 却会对整个 userInfo 对象做比较。

    假设我们只更新了 userInfo.level,useSelector 的比较结果就为 false 了,导致组件更新,即使你没有用上 level,这不是我们期望的。

    所以正确的写法应该是:

    const Comp() {
      const { username, age } = useSelector(state => {
        const { username, age } = state.userInfo;
        return { username, age };
      }), shallowEqual);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用 useSelector 监听状态变化,一定要关注 state 的粒度问题

    Context 是粗粒度的

    React 提供的 Context 的粒度是粗粒度的。

    当 Context 的值变化时,用到该 Context 的组件就会更新。

    有个问题,就是 我们提供的 Context 值通常都是一个对象,比如:

    const App = () => {
      return (
        
          
        
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    每当 Context 的 value 变化时,用到这个 Context 的组件都会被更新,即使你只是用这个 value 的其中一个属性,且它没有改变。

    因为 Context 是粗粒度的

    所以你或许可以考虑在高一些层级的组件去获取 Context,然后通过 props 分别注入到用到 Context 的不同部分的组件中

    顺便一提,Context 的 value 在必要时也要做缓存,以防止组件的无意义更新。

    const App = () => {
      const EditorContextVal = useMemo(() => ({ visible, setVisible }), [visible, setVisible]);
      return (
        
          
        
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    批量更新

    有一个经典的问题是:React 的 setState 是同步还是异步的?

    答案是副作用或合成事件响应函数内,是异步的,会批量执行。其他情况(比如 setTimeout)则会同步执行,同步的问题是会立即进行组件更新渲染,一次有多个同步 setState 就可能会有性能问题。

    我们可以用 ReactDOM.unstable_batchedUpdates 来将本来需要同步执行的状态更新变成批量的。

    ReactDOM.unstable_batchedUpdates(() => {
      setScore(score + 1);
      setUserName('前端西瓜哥');
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    不过到了 React18 后,开启并发模式的话,就没有同步问题了,所有的 setState 都是异步的。

    Redux 的话,你可以考虑使用批量更新插件:redux-batched-actions。

    import { batchActions } from 'redux-batched-actions';
    
    dispatch(batchActions([
      setScoreAction(score + 1),
      setUserName('前端西瓜哥')
    ]));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    redux-batched-actions 中间件确实会将多个 actions 做一个打包组合再 dispatch,你会发现 store.subscribe 的回调函数触发次数确实变少了。

    但如果你用了 react-redux 库的话,这个库其实在多数情况下并没有什么用。

    因为 react-redux 其实已经帮我们做了批量处理操作,同步的多个 dispatch 执行完后,才会通知组件进行重渲染。

    懒加载

    有些组件,如果可以的话,可以让组件直接不渲染,做一个懒加载。比如:

    {visible && }
    
    
    • 1
    • 2

    结尾

    React 的优化门道还是挺多的,其中的 React.memo 优化起来确实复杂,一不小心还会整成负优化。

    所以,不要 过早进行优化

    我是前端西瓜哥,欢迎关注我,学习更多前端知识。

  • 相关阅读:
    【JAVA入门】网络编程
    业绩走低,毛利率下滑,海外市场能否成为极米科技救命稻草?
    上海亚商投顾:沪指震荡调整跌 减肥药、华为概念股持续活跃
    情感分析系列(三)——使用TextCNN进行情感分析
    影视行业应该如何利用软文进行宣传?媒介盒子告诉你
    lv8 嵌入式开发-网络编程开发 16 多路复用poll函数
    fastadmin tp 安装使用百度富文本编辑器UEditor
    局域网的网络硬件主要包括有什么
    Django反向解析函数reverse与resolve
    Go语言环境安装
  • 原文地址:https://blog.csdn.net/fe_watermelon/article/details/126114787