React组件设计之性能优化篇

这篇具有很好参考价值的文章主要介绍了React组件设计之性能优化篇。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:空山

前言

由于笔者最近在开发中遇到了一个重复渲染导致子组件状态值丢失的问题,因此关于性能优化做了以下的分析,欢迎大家的交流

我们在日常的项目开发中往往会把页面拆分成一个个的组件,通过拼装的方式来实现整体的页面效果,所以与其说去优化 React,不如聚焦在现有的组件中,思考🤔如何去设计一个组件才能提高他的性能,从而提高整个项目的性能以及交互的流畅性。

回顾

在我们初学 React 的时候相信大家或多或少运行过这样的 demo:

import React, { useState } from 'react';
import { Button, Space } from 'antd';
import 'antd/dist/antd.css';
import './index.css';

const Parent: React.FC = () => {
  const [count, setCount] = useState(0);
  return (
    <Space direction="vertical">
      {count}
      <Button type="primary" onClick={() => setCount(count + 1)}>
        count + 1
      </Button>
      <Children />
    </Space>
  );
};

const Children: React.FC = () => {
  console.log('更新了子组件');
  return <div>这是子组件</div>;
};

export default Parent;

代码很少就是个简单的父组件嵌套子组件的情况,我们测试运行也没啥问题,大多数时候我们可能也就这么开发了。
但是我们观察下,每次当我们点击按钮进行 count + 1 时会更新子组件,这一点从控制台打印的信息可以看出。虽然 React 中有 diff 算法决定是否需要切实更新 DOM 元素,但是其内部的定义的一些函数还是会执行,而且 diff 会遍历整棵 virtualDOM 树也会有一定的性能消耗,那能不能优化下这个勒。

性能优化实践

由于 React 中的组件分为 Function 组件和 Class 组件,优化的手段根据其特性不同也分为两类

Function 组件

React.memo

React.memo 是 React 提供的一个高阶组件,用于优化组件的性能。它可以在某些情况下避免不必要的组件重新渲染,从而提高应用程序的性能。其使用方式分为两种:

  • 基础使用
    函数组件直接包裹 React.memo 默认使用浅层比较。
  • 高阶使用
    如果需要更精确地控制何时重新渲染组件,可以通过传递第二个参数给 React.memo 来指定自定义的比较函数。这个比较函数接收两个参数,分别是前一次的 props 和当前的 props ,返回一个布尔值表示是否需要重新渲染组件
import React from 'react';

const areEqual = (prevProps, nextProps) => {
  // 自定义比较逻辑
  // 返回 true 表示两个 props 相等,不需要重新渲染
  // 返回 false 表示两个 props 不相等,需要重新渲染
  return prevProps.value === nextProps.value;
};

const MyComponent = React.memo((props) => {
  console.log('Rendering MyComponent');
  return <div>{props.value}</div>;
}, areEqual);

使用useCallback

useCallback 是 React 中的一个 Hook,用于优化性能和避免不必要的渲染。它主要用于创建一个稳定的回调函数,并在依赖项未发生变化时缓存该函数。
示例代码:

import React, { useState, useCallback, useEffect } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    handleClick();
  }, [handleClick]);

  // 使用 useCallback 缓存回调函数 handleClick
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

注意⚠️:并不是必需的,它主要用于解决特定的性能问题。在大多数情况下,使用普通的函数定义也是有效的。只有在性能优化成为问题时,才需要考虑使用 useCallback。过度使用该useCallback会导致内存占用增加(每个缓存的回调函数都会占用内存),代码复杂度增加可读性变差并且难以维护。
可以遵循以下原则:

  • 只在需要时使用:只有在明确的性能问题存在时,或者需要将回调函数作为依赖项传递给其他 Hooks(如 useEffectuseMemo)时,才使用 useCallback
  • 明确指定依赖项:确保正确指定 useCallback 的依赖项数组,以确保缓存的回调函数在依赖项未发生变化时不会重新创建

使用useMemo

它用于在组件渲染过程中进行记忆化计算,以避免不必要的重复计算,提高应用的性能。
使用场景:

  • 计算昂贵的计算结果:涉及到需要执行昂贵的计算或处理大量数据的情况下,可以使用 useMemo 将计算结果缓存起来
  • 避免不必要的渲染:某个组件的渲染结果仅依赖于特定的输入参数,并且这些参数没有发生变化时,可以使用 useMemo 缓存该组件的输出,避免不必要的重新渲染
import React, { useMemo } from 'react';

const MyComponent = ({ data }) => {
  // 使用 useMemo 缓存结果
  const processedData = useMemo(() => {
    // 执行昂贵的计算或处理逻辑
    // 这里只是一个简单的示例,实际场景可能更复杂
    console.log('Processing data...');
    return data.map(item => item * 2);
  }, [data]); // 依赖项: 当 data 发生变化时重新计算

  return (
    <div>
      {/* 渲染使用 useMemo 缓存的结果 */}
      <ul>
        {processedData.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

Class 组件

巧用PureComponent

!!仅支持React 15.3及以上版本
PureComponent 是继承自 React.Component 的一个子类,它额外实现了shouldComponentUpdate 方法,并通过对组件的 props 和 state 进行浅层比较来确定是否需要重新渲染组件。
使用方式:

class App extends React.PureComponent

如果 props 和 state 没有发生改变,就不会进入 render 节点,省去了生成 Virtual DOM 和 Diff 的过程。
低版本可以使用PureRenderMixin,使用浅比较来决定是否应该触发组件的重新渲染。它会自动为组件添加一个 shouldComponentUpdate 方法,该方法会比较新的 propsstate 与当前的 propsstate,并根据比较结果决定是否重新渲染组件

import PureRenderMixin from 'react-addons-pure-render-mixin';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  // 组件的其他方法和生命周期函数
  // ...
}

合理使用shouldComponentUpdate

如果想对渲染进行更加细微的控制,或者是对引用类型进行渲染控制我们可以使用shouldComponentUpdate,通过返回 true  进行更新, false 阻止不必要的更新。

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 对比新旧属性和状态
    if (this.props.value === nextProps.value && this.state.count === nextState.count) {
      return false; // 属性和状态相同,不需要重新渲染
    }
    return true; // 需要重新渲染
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

特别注意⚠️:合理使用,手动实现 shouldComponentUpdate 可能会增加代码的复杂性,并且过度使用它可能会导致更多的维护问题。只有在确实需要优化性能时,才建议使用它。

在构造函数中绑定this

当我们在类组件中绑定类方法通过需要绑定他的 this 指向

// 方式一
render() {
  return <Button onClick={this.handleClick.bind(this)}>测试按钮</Button>
}

// 方式二
constructor() {
  super();
  this.handleClick = this.handleClick.bind(this);
}

虽然用起来是一样的,但是第一种方式在 render 的时候,每次会 bind this 生成新的函数实例,而第二种只会执行一次。

React中其他的优化手段

组件卸载时的清理

组件中注册的全局的监听器、定时器等,需要在组件卸载的时候进行清理,防止后续的执行影响性能以及内存泄露等问题

  • Class 组件:componentWillUnmount
  • Function 组件:useEffect return
import React, { useState, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 定义定时器
    const timer = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    const handleOnResize = () => {
	console.log('Window resized');
    }

    // 定义监听器
    const listener = window.addEventListener('resize', handleOnResize);

    // 在组件卸载时清除定时器和监听器
    return () => {
      clearInterval(timer);
      window.removeEventListener('resize', handleOnResize);
    };
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
}

export default Timer;

使用lazy进行组件懒加载

React版本支持16.6及以上版本
低版本可以考虑使用第三方库(如react-loadable)来实现类似的懒加载效果

通过组件懒加载可以将代码分割成更小的块,并且只有在需要时才会被加载
当用户访问某个特定页面时,只有与该页面相关的代码会被下载和执行,而其他代码则不会被加载。这样可以使应用程序更快地启动,并减少页面响应延迟。

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </Suspense>
    </Router>
  );
}

export default App;

在React中我们通常使用 lazySuspense 互相配合的方式进行懒加载,使用 lazy 方法来懒加载 HomeAbout 组件,使用Suspense可以在组件加载完成之前显示一个自定义的加载指示器或占位符,从而提高用户体验。

使用React Fragment减少额外节点的渲染

!!仅支持React 16.2及以上版本
React Fragment 允许你在 React 组件中返回多个元素而不需要添加额外的根节点,比如在某些情况下你的组件返回的是一个 list 的元素集合而他的根元素在父组件中,你想要的结构是根节点内直接元素的集合而不想再包裹一层,可以使用这种方式解决

javascript
import React, { lazy, Suspense } from 'react';

function TdList() {
  return (
    <React.Fragment>
      <td>Hello, World!</td>
      <td>This is a paragraph.</td>
    </React.Fragment>
  );
}

function Table() {
  return (
    <table>
      <tr>
	<TdList />
      </tr>
    </table>
  );
}

除此以外还具有一下优势:

  • 更清晰的代码结构:以片段的形式包裹元素可读性更高,避免了成片的div
  • 减少 DOM 层级:减少渲染出来的 DOM 层级,从而提高性能
  • 更符合预期:更容易使我们按照预期呈现出想要表达的组件特别是在便利的时候,而不会引起元素的父子关系问题

有时候我们也会使用<></>,被称为空标签或者隐式 Fragment,其用法和Fragment相同,唯一的区别在于空标签不能添加任何属性,而后者可以,比如说在某些场景下需要给父元素增加key值。此时只能使用Fragment

减少使用通过内联函数绑定事件

当我们在 React 中使用内联函数时,每次重新 render 将导致生成新的函数实例从而为元素绑定新的函数,在非嵌套的组件使用时影响不大,但是如果存在嵌套组件并且该内联函数是作为 props 传递给子组件时将会导致子组件重新渲染,即使内联函数里的代码相同的情况下。

  • Class 组件:将内联函数定义为类方法传递给子组件
  • Function 组件:useCallback进行缓存

// bad
import React, { useCallback } from 'react';

function MyComponent() {
  return (
    <button 
	onClick={() => { 
	  // 处理点击事件 
	}}
     >
	Click me
    </button>
  );
}

// good
import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // 处理点击事件
  }, []);

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

使用key提升列表的渲染性能

假设你有一个需要渲染大量数据的列表组件,每个列表项都是一个独立的子组件。当你对这个列表进行添加、删除或重新排序操作时,React 需要计算出哪些子组件需要更新。

import React from 'react';

function MyComponent(props) {
  const data = props.data; // 假设这是一个包含大量数据的数组

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

未使用 key 的情况下,React 将无法区分列表项之间的差异,而会重新渲染整个列表。使用 key 可以帮助 React 提高性能,它可以通过比较新旧的 key 来确定是否需要更新特定的列表项。这样,当你对列表进行操作时,React 只会针对变化的部分进行更新。
注意⚠️:尽量不要使用 index 作为 key 值

通用的优化手段

可见性加载

在我们浏览很多图像、卡片、列表时我们往往不会立马加载所有的资源,主要是因为一个是很浪费资源,另一个就是在不可见区域内也必要进行加载。借鉴这个思想,我们是否可以在组件在视口范围内进行加载呢,其余不进行加载——IntersectionObserver 或者是一些现有的库如react-loadable-visibility(通过 react-loadable 按需加载组件 + Intersection Observer API 监听组件的可见性)

import React from "react";
import { Button } from "antd";
import LoadableVisibility from "react-loadable-visibility/react-loadable";

const LoadingComponent = () => <div>Loading...</div>;

const MyComponent = LoadableVisibility({
  loader: () => import("./MyComponent"),
  loading: LoadingComponent
});

const App = () => {
  return (
    <div>
      <h1>My App</h1>
      <MyComponent />
    </div>
  );
};

export default App;

当 MyComponent 组件进入视口时,它们才会被加载和渲染,而在加载过程中,会显示 LoadingComponent 组件作为占位符,需要注意的是,确保在支持 Intersection Observer API 的浏览器中进行

交互式导入资源

页面中包含并非立即需要的组件或资源的代码或数据,立即加载这些资源将阻塞主线程,而这些功能当用户不去触发某些操作是也是用不到的,就可以采用这种方式。
比如我们有个需求是点击“滚动到顶部”按钮时以动画方式滚动回页面顶部,这里我们用到了react-scroll 这个包,那可以在与按钮交互时加载它

handleScrollToTop() {
  import('react-scroll').then(scroll => {
    scroll.animateScroll.scrollToTop({
    })
  })
}

Web Worker

通过 Web Worker 创建多线程的环境,主线程把一些任务分配给后者运行,不会阻塞主线程的运行,使交互更加流畅。
适用场景:

  • 计算密集型或高延迟的任务

虚拟列表

仅渲染可见区域的 dom 元素而不必要渲染全部,提高渲染性能。可以使用 React-virtualized 或者是 React-window 等包。

总结

以上就是笔者对性能优化方面的研究🧐和总结,如果大家在日常开发中有这样的诉求可以参考以上几种方式。

参考资料:

  • https://www.patterns.dev/posts#design-patterns
  • https://juejin.cn/post/7142880902551437342#heading-19

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star文章来源地址https://www.toymoban.com/news/detail-604339.html

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko

到了这里,关于React组件设计之性能优化篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 理解React页面渲染原理,如何优化React性能?

    当使用React编写应用程序时,可以使用JSX语法来描述用户界面的结构。JSX是一种类似于HTML的语法,但实际上它是一种JavaScript的扩展,用于定义React元素。React元素描述了我们想要在界面上看到的内容和结构。 在运行React应用程序时,JSX会被转换成真实的DOM元素,这个过程主要

    2024年02月08日
    浏览(43)
  • 5.React.memo 性能优化

    性能优化, React.memo 2. React.memo类似纯组件,可提高组件性能表现(类组件PureComponent)

    2024年02月11日
    浏览(35)
  • React Native性能优化指南

    本文将介绍在React Native开发中常见的性能优化问题和解决方案,包括ScrollView内无法滑动、热更新导致的文件引用问题、高度获取、强制横屏UI适配、低版本RN适配iOS14、缓存清理、navigation参数取值等。通过代码案例演示和详细说明,帮助开发者更好地理解和解决React Native中的

    2024年01月23日
    浏览(46)
  • React Hooks ——性能优化Hooks

    Hooks从语法上来说是一些函数。这些函数可以用于在函数组件中引入状态管理和生命周期方法。 简洁 从语法上来说,写的代码少了 上手非常简单 基于函数式编程理念,只需要掌握一些JavaScript基础知识 与生命周期相关的知识不用学,react Hooks使用全新的理念来管理组件的运作

    2024年02月06日
    浏览(42)
  • React性能优化之memo缓存函数

    React是一个非常流行的前端框架,但是在处理大型应用程序时,性能可能会成为一个问题。为了解决这个问题,React提供了一个称为memo的功能,它可以缓存函数并避免不必要的重新渲染。 memo是React中的一个高阶组件(HOC),它接收一个组件并返回一个新的组件。这个新组件具

    2024年02月11日
    浏览(36)
  • React性能优化之Memo、useMemo

    React 的渲染机制,组件内部的 state 或者 props 一旦发生修改,整个组件树都会被重新渲染一次,即时子组件的参数没有被修改,甚至无状态组件 会造成性能浪费 React.memo 是 React 官方提供的一个高阶组件,用于缓存我们的需要优化的组件 React 中的组件被设计为在状态或 props 值

    2024年02月14日
    浏览(38)
  • React Hook之useCallback 性能优化

    上文 对比之前的组件优化说明React.memo的作用我们说了 React.memo的妙用 但是 它却并非万能 我们来看这个情况 我们子组件代码编写如下 这里 我们接收了父组件 props中的一个 dom1funt 函数 然后点击dom1funt按钮 触发这个dom1funt 然后 父组件代码编写如下 父组件 我们定义了这个传给

    2024年02月11日
    浏览(54)
  • 【前端知识】React 基础巩固(二十三)——React 性能优化 SCU相关

    React 的渲染流程 JSX - 虚拟 DOM - 真实 DOM React 的更新流程 props/state 改变 - render函数重新执行 - 产生新的DOM树 - 新旧DOM树进行diff - 计算出差异进行更新 - 更新到真实的DOM React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树 React 需要基于这两颗不同的

    2024年02月15日
    浏览(71)
  • React【性能优化_shouldComponentUpdate、性能优化_时间分片、性能优化_虚拟列表、PropTypes 进行类型检查、默认 Prop 值、 TypeScript】(六)

    目录 性能优化_shouldComponentUpdate 性能优化_时间分片 性能优化_虚拟列表

    2024年02月08日
    浏览(55)
  • 关于 React 性能优化和数栈产品中的实践

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 本文作者:的卢 在日常开发过程中,我们会使用很多性能优化的 API ,比如像使用 memo 、 useMemo 优化组件或者值,再比如使用 shouldCo

    2024年02月08日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包