我们是袋鼠云数栈 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 等包。

总结

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

参考资料:


最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

React组件设计之性能优化篇的更多相关文章

  1. React组件设计

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  2. React组件设计(转)

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  3. React组件设计技巧

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  4. 秋色园QBlog技术原理解析:性能优化篇:缓存总有失效时,构造持续的缓存方案(十四)

    转载自:http://www.cyqdata.com/qblog/article-detail-38993 文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文 ...

  5. jQuery性能优化篇

    jQuery高级技巧——性能优化篇 阅读目录 通过CDN(Content Delivery Network)引入jQuery库 减少DOM操作 适当使用原生JS 选择器优化 缓存jQuery对象 定义 ...

  6. MySQL性能调优与架构设计——第9章 MySQL数据库Schema设计的性能优化

    第9章 MySQL数据库Schema设计的性能优化 前言: 很多人都认为性能是在通过编写代码(程序代码或者是数据库代码)的过程中优化出来的,其实这是一个非常大的误区.真正影响性能最大的部分是在设计中就 ...

  7. 《MySQL性能优化篇》阅读笔记

    建表的时候,不要用null赋默认值,如:字符串的设置'',数据类型的设为0,不要将null设为默认值. 在MySQL中没有 full [outer] join,用union代替 各种 JOIN SQL ...

  8. JavaScript性能优化篇js优化

    JavaScript性能优化篇js优化   随着Ajax越来越普遍,Ajax引用的规模越来越大,Javascript代码的性能越来越显得重要,我想这就是一个很典型的例子,上面那段代码因为会被频繁使用, ...

  9. 【React】393 深入了解React 渲染原理及性能优化

    如今的前端,框架横行,出去面试问到框架是常有的事. 我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家. React 是什么 React是一个专注 ...

  10. React 16 加载性能优化指南

    关于 React 应用加载的优化,其实网上类似的文章已经有太多太多了,随便一搜就是一堆,已经成为了一个老生常谈的问题. 但随着 React 16 和 Webpack 4.0 的发布,很多过去的优化手段 ...

随机推荐

  1. .Net性能测试工具BenchmarkDotNet学习

    .Net性能测试工具BenchmarkDotNet学习 BenchmarkDotNet 是一个用于性能基准测试的开源框架.它可以让开发人员编写简单易懂的代码,并测量和分析这些代码的性能表现,从而帮助开 ...

  2. error while loading shared libraries: libstdc++.so.6: cannot open shared object file: No such file o

    error while loading shared libraries: libstdc++.so.6: cannot open shared object file: No such file o ...

  3. [C++提高编程] 3.7 list容器

    文章目录 3.7 list容器 3.7.1 list基本概念 3.7.2 list构造函数 3.7.3 list 赋值和交换 3.7.4 list 大小操作 3.7.5 list 插入和删除 3.7. ...

  4. vue页面中展示markdown以及katex公式

    场景 数据库中有markdown语法的字符串,需要展示为正常的页面,难点在于其中的katex数学公式 解决方式 使用showdown及其族系插件 npm i showdown npm i showdo ...

  5. Visual Basic 6 API压缩数据

    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As ...

  6. 2020-11-15:手写代码:行有序、列也有序的二维数组中,找num,找到返回true,否则false?

    福哥答案2020-11-15: 此题来源于leetcode240和剑指 Offer(第 2 版)面试题4.1.线性查找.从二维数组的坐下角开始查找.如果当前元素等于目标值,则返回 true.如果当前元 ...

  7. linux 引导过程和服务控制

    目录 一.引导分区 二.服务控制 三.运行级别 四.systemd初始化 五.模拟错误 一.引导分区 原理:引导分区是指在开机启动到进入系统这之间的过程 引导分区的过程:1.开机自检 自检顺序:BIO ...

  8. ODOO前端引用css如何修改页面属性

    odoo前端存在一些样式不合理的地方,如何通过ccs修改页面属性: 1  通过页面属性class: 2  新建模块后,创建static/src/css/styles.css文件 3  style.cs ...

  9. 龙芯下如何进行.net core程序开发部署

    龙芯LoongArch64已经发布了对.NETCore的支持,相关工具链也已完成,目前支持.NETCore3.1..NET6..NET7版本.本文以.NETCore3.1在loongnix-serve ...

  10. 全志G2D实现屏幕旋转,开机logo实现手动旋转。

    产品设计出来之后啊,大家使用的时候觉得反过来使用更加便捷.但是屏幕显示是反的.那怎么办那????? 修改硬件费时费工,那能否软件实现那????? 如果纯软件使用那就太费系统资源了.于是就想到了使用全志 ...