React 组件性能优化最佳实践
React 组件性能优化的核心是减少渲染真实 DOM 节点的频率,减少 Virtual DOM 比对的频率。如果子组件未发生数据改变不渲染子组件。
组件卸载前进行清理操作
以下代码在组件挂载时会创建一个 interval 组件销毁后清除定时器,间隔 1 秒会触发渲染count+1
,组件销毁后如果不清除定时器它会一直消耗资源
import React, { useState, useEffect } from "react"
import ReactDOM from "react-dom"
const App = () => {
let [index, setIndex] = useState(0)
useEffect(() => {
let timer = setInterval(() => {
setIndex(prev => prev + 1)
console.log('timer is running...')
}, 1000)
return () => clearInterval(timer)
}, [])
return (
<button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))}> {index} </button>
)
}
export default App
复制代码
每次数据更新都会触发组件重新渲染,这里的优化为:组件销毁清理定时器
类组件使用纯组件PureComponent
什么是纯组件
纯组件会对组件输入数据进行浅层比较,如果当前输入数据和上次输入数据相同,组件不会重新渲染
什么是浅层比较
比较引用数据类型在内存中的引用地址是否相同,比较基本数据类型的值是否相同。
为什么不直接进行 diff 操作, 而是要先进行浅层比较,浅层比较难道没有性能消耗吗
和进行 diff 比较操作相比,浅层比较将消耗更少的性能。diff 操作会重新遍历整颗 virtualDOM 树, 而浅层比较只操作当前组件的 state 和 props。
import React from "react"
export default class App extends React.Component {
constructor() {
super()
this.state = {name: "张三"}
}
updateName() {
setInterval(() => this.setState({name: "张三"}), 1000)
}
componentDidMount() {
this.updateName()
}
render() {
return (
<div>
<RegularComponent name={this.state.name} />
<PureChildComponent name={this.state.name} />
</div>
)
}
}
class RegularComponent extends React.Component {
render() {
console.log("RegularComponent")
return <div>{this.props.name}</div>
}
}
class PureChildComponent extends React.PureComponent {
render() {
console.log("PureChildComponent")
return <div>{this.props.name}</div>
}
}
复制代码
组件挂载以后会有一个定时器间隔 1 秒设置一次name
,我们可以看到RegularComponent
一直在渲染,即使数据没有发生变化也会渲染。PureChildComponent
只有一次渲染,因此使用纯组件会对props
state
进行进行比较,数据相同不会重新渲染。
shouldComponentUpdate
纯组件只能进行浅层比较,要进行深层比较,使用 shouldComponentUpdate,它用于编写自定义比较逻辑。
返回 true 重新渲染组件,返回 false 阻止重新渲染。
函数的第一个参数为 nextProps, 第二个参数为 nextState。
import React from "react"
export default class App extends React.Component {
constructor() {
super()
this.state = {name: "张三", age: 20, job: "waiter"}
}
componentDidMount() {
setTimeout(() => this.setState({ job: "chef" }), 1000)
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.name !== nextState.name || this.state.age !== nextState.age) {
return true
}
return false
}
render() {
console.log("rendering")
let { name, age } = this.state
return <div>{name} {age}</div>
}
}
复制代码
即使继承了Component
的组件定时器一直修改数据也不会触发重新渲染
纯函数组件使用React.memo
优化性能
memo 基本使用
将函数组件变为纯组件,将当前 props 和上一次的 props 进行浅层比较,如果相同就阻止组件重新渲染。
import React, { memo, useEffect, useState } from "react"
function ShowName({ name }) {
console.log("showName render...")
return <div>{name}</div>
}
const ShowNameMemo = memo(ShowName)
function App() {
const [index, setIndex] = useState(0)
const [name] = useState("张三")
useEffect(() => {
setInterval(() => {
setIndex(prev => prev + 1)
}, 1000)
}, [])
return (
<div>
{index} <ShowNameMemo name={name} />
</div>
)
}
export default App
复制代码
memo 传递比较逻辑(使用 memo 方法自定义比较逻辑,用于执行深层比较。)
import React, { memo, useEffect, useState } from "react";
function ShowName({ person }) {
console.log("showName render...");
return (
<div>
{person.name} 丨 {person.job} </div>
);
}
function comparePerson(prevProps, nextProps) {
if (
prevProps.person.name !== nextProps.person.name ||
prevProps.person.age !== nextProps.person.age
) {
return false
}
return true
}
const ShowNameMemo = memo(ShowName, comparePerson);
function App() {
const [person, setPerson] = useState({ name: "张三", job: "developer" });
useEffect(() => {
setInterval(() => {
setPerson((data) => ({ ...data, name: "haoxuan" }));
}, 1000);
}, []);
return (
<div>
<ShowNameMemo person={person} />
</div>
);
}
export default App;
复制代码
使用组件懒加载
使用组件懒加载可以减少 bundle 文件大小, 加快组件呈递速度。
参考 前端进阶面试题详细解答
路由组件懒加载
import React, { lazy, Suspense } from "react"
import { BrowserRouter, Link, Route, Switch } from "react-router-dom"
const Home = lazy(() => import(/* webpackChunkName: "Home" */ "./Home"))
const List = lazy(() => import(/* webpackChunkName: "List" */ "./List"))
function App() {
return (
<BrowserRouter>
<Link to="/">Home</Link>
<Link to="/list">List</Link>
<Switch>
<Suspense fallback={<div>Loading</div>}> <Route path="/" component={Home} exact />
<Route path="/list" component={List} />
</Suspense>
</Switch>
</BrowserRouter>
)
}
export default App
复制代码
根据条件进行组件懒加载(适用于组件不会随条件频繁切换)
import React, { lazy, Suspense } from "react"
function App() {
let LazyComponent = null
if (true) {
LazyComponent = lazy(() => import(/* webpackChunkName: "Home" */ "./Home"))
} else {
LazyComponent = lazy(() => import(/* webpackChunkName: "List" */ "./List"))
}
return (
<Suspense fallback={<div>Loading</div>}> <LazyComponent />
</Suspense>
)
}
export default App
复制代码
使用 Fragment 避免额外标记
为了满足这个条件我们通常都会在最外层添加一个 div, 但是这样的话就会多出一个无意义的标记, 如果每个组件都多出这样的一个无意义标记的话, 浏览器渲染引擎的负担就会加剧。
import { Fragment } from "react"
function App() {
return (
<Fragment>
<div>message a</div>
<div>message b</div>
</Fragment>
)
}
复制代码
function App() {
return (
<>
<div>message a</div>
<div>message b</div>
</>
)
}
复制代码
不要使用内联函数定义
在使用内联函数后, render 方法每次运行时都会创建该函数的新实例, 导致 React 在进行 Virtual DOM 比对时, 新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例, 而旧的函数实例又要交给垃圾回收器处理。
错误示范:
import React from "react"
export default class App extends React.Component {
constructor() {
super()
this.state = {
inputValue: ""
}
}
render() {
return (
<input
value={this.state.inputValue}
onChange={e => this.setState({ inputValue: e.target.value })} />
)
}
}
复制代码
正确的做法是在组件中单独定义函数, 将函数绑定给事件:
import React from "react"
export default class App extends React.Component {
constructor() {
super()
this.state = {
inputValue: ""
}
}
setInputValue = e => {
this.setState({ inputValue: e.target.value })
}
render() {
return (
<input value={this.state.inputValue} onChange={this.setInputValue} />
)
}
}
复制代码
在构造函数中进行函数 this 绑定
在类组件中如果使用 fn() {} 这种方式定义函数, 函数 this 默认指向 undefined. 也就是说函数内部的 this 指向需要被更正.
可以在构造函数中对函数的 this 进行更正, 也可以在行内进行更正, 两者看起来没有太大区别, 但是对性能的影响是不同的
export default class App extends React.Component {
constructor() {
super()
// 方式一
// 构造函数只执行一次, 所以函数 this 指向更正的代码也只执行一次.
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this)
}
render() {
// 方式二
// 问题: render 方法每次执行时都会调用 bind 方法生成新的函数实例.
return <button onClick={this.handleClick.bind(this)}>按钮</button>
}
}
复制代码
类组件中的箭头函数
在类组件中使用箭头函数不会存在 this 指向问题, 因为箭头函数本身并不绑定 this。
export default class App extends React.Component {
handleClick = () => console.log(this)
render() {
return <button onClick={this.handleClick}>按钮</button>
}
}
复制代码
箭头函数在 this 指向问题上占据优势, 但是同时也有不利的一面.
当使用箭头函数时, 该函数被添加为类的实例对象属性, 而不是原型对象属性. 如果组件被多次重用, 每个组件实例对象中都将会有一个相同的函数实例, 降低了函数实例的可重用性造成了资源浪费.
综上所述, 更正函数内部 this 指向的最佳做法仍是在构造函数中使用 bind 方法进行绑定
优化条件渲染
频繁的挂载和卸载组件是一项耗性能的操作, 为了确保应用程序的性能, 应该减少组件挂载和卸载的次数.
在 React 中我们经常会根据条件渲染不同的组件. 条件渲染是一项必做的优化操作。
function App() {
if (true) {
return (
<>
<AdminHeader />
<Header />
<Content />
</>
)
} else {
return (
<>
<Header />
<Content />
</>
)
}
}
复制代码
在上面的代码中, 当渲染条件发生变化时, React 内部在做 Virtual DOM 比对时发现, 刚刚第一个组件是 AdminHeader, 现在第一个组件是 Header, 刚刚第二个组件是 Header, 现在第二个组件是 Content, 组件发生了变化, React 就会卸载 AdminHeader、Header、Content, 重新挂载 Header 和 Content, 这种挂载和卸载就是没有必要的。
function App() {
return (
<>
{true && <AdminHeader />} <Header />
<Content />
</>
)
}
复制代码
避免使用内联样式属性
当使用内联 style 为元素添加样式时, 内联 style 会被编译为 JavaScript 代码, 通过 JavaScript 代码将样式规则映射到元素的身上, 浏览器就会花费更多的时间执行脚本和渲染 UI, 从而增加了组件的渲染时间。
function App() {
return <div style={{ backgroundColor: "skyblue" }}>App works</div>
}
复制代码
避免重复无限渲染
当应用程序状态发生更改时, React 会调用 render 方法, 如果在 render 方法中继续更改应用程序状态, 就会发生 render 方法递归调用导致应用报错.
export default class App extends React.Component {
constructor() {
super()
this.state = {name: "张三"}
}
render() {
this.setState({name: "李四"})
return <div>{this.state.name}</div>
}
}
复制代码
与其他生命周期函数不同, render 方法应该被作为纯函数. 这意味着, 在 render 方法中不要做以下事情, 比如不要调用 setState 方法, 不要使用其他手段查询更改原生 DOM 元素, 以及其他更改应用程序的任何操作. render 方法的执行要根据状态的改变, 这样可以保持组件的行为和渲染方式一致.
避免数据结构突变
组件中 props 和 state 的数据结构应该保持一致, 数据结构突变会导致输出不一致.
import React, { Component } from "react"
export default class App extends Component {
constructor() {
super()
this.state = {
employee: {
name: "张三",
age: 20
}
}
}
render() {
const { name, age } = this.state.employee
return (
<div>
{name} {age} <button
onClick={() =>
this.setState({ ...this.state, employee: { ...this.state.employee, age: 30 } }) } > change age </button>
</div>
)
}
}
复制代码
评论