【React 系列】React 中的性能优化

前言

在 React 中当我们的父组件更新时,将渲染整个子组件树,这样会造成很大的性能开销,所以我们需要对组件进行优化,避免不必要的渲染。

常见的性能优化手段有:

  1. 使用 useMemo 缓存数据、使用 useCallback 缓存函数
  2. 使用 React.memo 缓存组件
  3. 合理的使用 Key
  4. 在组件销毁的时候清除定时器/事件
  5. 通过 Suspense 和 Lazy 拆分组件
  6. 使用 Fragment 避免额外标记
  7. 避免使用内联函数
  8. 避免使用内联样式
  9. 优化渲染条件
  10. 避免重复渲染
  11. 为组件创建错误边界

React 中的性能优化

1 使用 useMemo 缓存数据、使用 useCallback 缓存函数

jsx 复制代码
/**
 * 使用 useMemo 缓存数据,类似于 Vue 的 computed 计算属性
 * 使用 useCallback 缓存函数
 * @returns
 */
const data = {
  userName: '张三',
  age: 19,
  fav: '篮球、排球',
};

const getUserInfo = () => {
  return {
    ...data,
    random: Math.random(),
  };
};
function Case2() {
  const [count, setCount] = React.useState(0);
  // 因为使用了 useMemo,页面刷新时,只会执行一次,随机数不会变
  const userInfo = useMemo(() => getUserInfo(), []);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div className={styles.case2page}>
      <div className={styles.content}>
        <div>姓名:{userInfo.userName}</div>
        <div>年龄:{userInfo.age}</div>
        <div>爱好:{userInfo.fav}</div>
        <div>随机数: {userInfo.random}</div>
        <div>当前页面渲染次数: {count}</div>
        <Button onClick={handleClick}>刷新渲染组件</Button>
      </div>
    </div>
  );
}

2 使用 React.memo 缓存组件

React.memo 类似于 shouldComponentUpdate,当 props 没有变化时,不会重新渲染组件,从而提高性能。

React.memo 返回 false 的时候,会重新渲染组件,返回 true 的时候,不会重新渲染组件。

shouldComponentUpdate 返回 false 的时候,不会重新渲染组件,返回 true 的时候,会重新渲染组件。

jsx 复制代码
import React from 'react';
import { Button } from 'antd';

const BasicButton = (props) => {
  return <Button {...props}></Button>;
};

export default React.memo(BasicButton, (oldProps, newProps) => {
  return oldProps === newProps; // true - 不更新 false - 更新
});

React.memo 内部将使用 Object.is 来比较 props.

Object.is 中 NaN 和 NaN 是相等的,+0 与 -0 不相等的

jsx 复制代码
function ObjectIs(value1, value2) {
    if (value1 === value2) {
        return value1 !== 0 || 1 / value1 === 1 / value2;
    }

    return value1 !== value1 && value2 !== value2;
}

3 合理的使用 Key

在 For 循环中尽量使用唯一的标识id作为key,避免使用index作为key,方便复用组件。

jsx 复制代码
import React from 'react';

/**
 * for 循环中合理的使用key
 * @returns
 */

const list = [
  {
    id: 1,
    name: '张三',
  },
  {
    id: 2,
    name: '李四',
  },
  {
    id: 3,
    name: '王五',
  },
];
function Case3() {
  return (
    <div>
      {list.map((item) => {
        return <div key={item.id}>{item.name}</div>;
      })}
    </div>
  );
}

export default Case3;

4 在组件销毁的时候清除定时器/事件

组件卸载的时候清除相关事件、定时器

jsx 复制代码
import React, { useEffect, useState } from 'react';

/**
 * 组件卸载的时候清除相关事件、定时器
 * @returns
 */
function Case1() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, [count]);

  return <div>{count}</div>;
}

export default Case1;

5 通过 Suspense 和 Lazy 异步加载组件

jsx 复制代码
import React, { lazy } from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const LearnReactOptimize = lazy(() => import('./pages/LearnReactOptimize'));

const LazyBoundary = (WrapComp) => (
  <Suspense fallback={<div>loading....</div>}>
    <WrapComp />
  </Suspense>
);

const routeConfig = createBrowserRouter([
  {
    path: '/LearnReactOptimize',
    element: LazyBoundary(LearnReactOptimize),
  },
  {
    path: '/',
    element: <App />,
  },
]);


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={routeConfig} />
  </React.StrictMode>,
);

6 使用 Fragment 避免额外标记

通过 Fragment 减少不必要的标签

jsx 复制代码
import React from 'react';

// bad
function App() {
  return (
    <div>
      <div>1</div>
      <div>2</div>
    </div>
  )
}

// good
function App() {
  return (
    <>
      <div>1</div>
      <div>2</div>
    </>
  )
}

7 避免使用内联函数

在 React 中每次 render 都会重新生成新的内联函数,导致在 diff 过程中,发现新旧函数不相等,又重新给组件挂载上新的函数,造成不必要的性能开销。

jsx 复制代码
// bad
function App() {
  const handleClick = () => {
    console.log('click')
  }

  return <div onClick={() => handleClick()}> App </div>
}

// good
function App() {
  const handleClick = () => {
    console.log('click')
  }

  return <div onClick={handleClick}> App </div>
}

8 避免使用内联样式属性

当使用内联 style 为元素添加样式时,内联 style 会被编译成 JavaScript 代码,通过 JavaScript 代码将样式规则映射到元素的身上,浏览器就会花费更多的时间执行脚本和渲染UI,从而增加了组件的渲染时间。

jsx 复制代码
function App() {
  return <div style={{ backgroundColor: "skyblue" }}>App works</div>
}

9 优化渲染条件

对于频繁渲染的组件,可以通过 display: none; 进行组件的显示隐藏,避免不必要的性能开销。

10 避免重复渲染

在函数组件中,避免在函数体中更改状态 在类组件中,避免在 render 函数内更改状态

jsx 复制代码
// bad
export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "张三"}
  }
  render() {
    this.setState({name: "李四"})
    return <div>{this.state.name}</div>
  }
}


// or
// bad
export default function App() {
  const [count, setCount] = count(0)
  setCount(count++)
  return <div>{count}</div>
}

11 为组件创建错误边界

当组件发生错误时,可以通过错误边界捕获错误,从而避免整个组件树的崩溃。

jsx 复制代码
export default class ErrorBoundary extends React.Component {
  constructor () {
    super();
    this.state = { hasError: false };
  }
  /**
   * 此生命周期在后代组件引发错误后调用
   * 返回的值会更新 state 状态
   */
  static getDerivedStateFromError() {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 捕获错误日志,将错误日志上报到服务器
    console.log(error, errorInfo)
  }

  render () {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

参考文章

  1. juejin.cn/post/696574...
  2. cloud.tencent.com/developer/a...
相关推荐
小小小小宇几秒前
前端用户行为监控
前端
潘小磊3 分钟前
高频面试之6Hive
大数据·hive·面试·职场和发展
步行cgn13 分钟前
Vue 事件修饰符详解
前端·javascript·vue.js
vvilkim22 分钟前
Flutter 状态管理基础:深入理解 setState 和 InheritedWidget
前端·javascript·flutter
Magnum Lehar30 分钟前
wpf3d游戏引擎前端ControlTemplate实现
前端·游戏引擎·wpf
程序员小张丶38 分钟前
基于React Native的HarmonyOS 5.0房产与装修应用开发
javascript·react native·react.js·房产·harmonyos5.0
丘山子1 小时前
别再滥用 None 了!这才是 Python 处理缺失值的好方法
后端·python·面试
早该学学了1 小时前
el-tabs问题解决大总结
前端
Sun_light1 小时前
LeetCode 59.「螺旋矩阵」
javascript·算法·面试
星河丶1 小时前
useEffect的清理函数的执行时机
前端·react.js