前言
我们在日常的项目开发中往往会把页面拆分成一个个的组件,通过拼装的方式来实现整体的页面效果,所以与其说去优化 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(如
useEffect
、useMemo
)时,才使用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
方法,该方法会比较新的 props
和 state
与当前的 props
和 state
,并根据比较结果决定是否重新渲染组件
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进行组件懒加载
通过组件懒加载可以将代码分割成更小的块,并且只有在需要时才会被加载
当用户访问某个特定页面时,只有与该页面相关的代码会被下载和执行,而其他代码则不会被加载。这样可以使应用程序更快地启动,并减少页面响应延迟。
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中我们通常使用 lazy
和 Suspense
互相配合的方式进行懒加载,使用 lazy
方法来懒加载 Home
和 About
组件,使用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