react 性能优化
React 组件性能优化的外围就是缩小渲染实在 DOM 节点的频率,缩小 Virtual DOM 比照的频率,以此来进步性能
1. 组件卸载之前进行清理操作
在组件中为 window 注册的全局事件,以及定时器,在组件卸载前要清理掉,避免组件卸载后继续执行影响利用性能
咱们开启一个定时器而后卸载组件,查看组件中的定时器是否还在运行 Test 组件来开启一个定时器
import {useEffect} from 'react'
export default function Test () {useEffect(() => {setInterval(() => {console.log('定时器开始执行')
}, 1000)
}, [])
return <div>Test</div>
}
在 App.js 中引入定时器组件而后用 flag 变量来管制渲染和卸载组件
import Test from "./Test";
import {useState} from "react"
function App() {const [flag, setFlag] = useState(true)
return (
<div>
{flag && <Test />}
<button onClick={() => setFlag(prev => !prev)}> 点击按钮 </button>
</div>
);
}
export default App;
在浏览器中咱们去点击按钮发现组件被卸载后定时器还在执行,这样组件太多之后或者这个组件不停的渲染和卸载会开启很多的定时器,咱们利用的性能必定会被拉垮,所以咱们须要在组建卸载的时候去销毁定时器。
import {useEffect} from 'react'
export default function Test () {useEffect(() => {
// 因为要销毁定时器所以咱们须要用一个变量来承受定时器 id
const InterValTemp = setInterval(() => {console.log('定时器开始执行')
}, 1000)
return () => {console.log(`ID 为 ${InterValTemp}定时器被销毁了 `)
clearInterval(InterValTemp)
}
}, [])
return <div>Test</div>
}
这个时候咱们在去点击销毁组建的时候定时器就被销毁掉了
2. 类组件用纯组件来晋升组建性能PureComponent
1. 什么是纯组件
纯组件会对组建的输出数据进行浅层比拟,如果输出数据和上次输出数据雷同,组建不会被从新渲染
2. 什么是浅层比拟
比拟援用数据类型在内存中的援用地址是否雷同,比拟根本数据类型的值是否雷同
3. 如何实现纯组件
类组件集成 PureComponent 类,函数组件应用 memo 办法
4. 为什么不间接进行 diff 操作,而是要进行浅层比拟,浅层比拟难到没有性能耗费吗
和进行 diff 比拟操作相比,浅层比拟小号更少的性能,diff 操作会从新遍历整个 virtualDOM 树,而浅层比拟只比拟操作以后组件的 state 和 props
在状态中存储一个 name 为张三的,在组建挂载后咱们每隔 1 秒更改 name 的值为张三,而后咱们看纯组件和非纯组件,查看后果
// 纯组件
import {PureComponent} from 'react'
class PureComponentDemo extends PureComponent {render () {console.log("纯组件")
return <div>{this.props.name}</div>
}
}
// 非纯组件
import {Component} from 'react'
class ReguarComponent extends Component {render () {console.log("非纯组件")
return <div>{this.props.name}</div>
}
}
引入纯组件和非纯组件 并在组件挂在后开启定时器每隔 1 秒更改 name 的值为张三
import {Component} from 'react'
import {ReguarComponent, PureComponentDemo} from './PureComponent'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
}
updateName () {setInterval(() => {this.setState({name: "张三"})
}, 1000)
}
componentDidMount () {this.updateName()
}
render () {
return <div>
<ReguarComponent name={this.state.name}></ReguarComponent>
<PureComponentDemo name={this.state.name}></PureComponentDemo>
</div>
}
}
关上浏览器查看执行后果
咱们发现纯组件只执行了一次,当前在改雷同的值的时候,并没有再从新渲染组件,而非纯组件则是每次更改都在从新渲染,所以纯组件要比非纯组件更节约性能
3. 函数组件来实现纯组件 memo
-
memo 根本应用
将函数组件变成纯组件,将以后的 props 和上一次的 props 进行浅层比拟,如果雷同就组件组件的渲染。》。
咱们在父组件中保护两个状态,index 和 name 开启定时器让 index 一直地发生变化,name 传递给子组件,查看父组件更新子组件是否也更新了, 咱们先不必 memo 来查看后果
import {useState, useEffect} from 'react'
function App () {const [ name] = useState("张三")
const [index, setIndex] = useState(0)
useEffect(() => {setInterval (() => {setIndex(prev => prev + 1)
}, 1000)
}, [])
return <div>
{index}
<ShowName name={name}></ShowName>
</div>
}
function ShowName ({name}) {console.log("组件被更新")
return <div>{name}</div>
}
关上浏览器查看执行后果
在不应用 memo 来把函数组件变成纯组件的状况下咱们发现子组件随着父组件更新而一起从新渲染,然而它依赖的值并没有更新,这样节约了性能,咱们应用 memo 来防止没必要的更新
import {useState, useEffect, memo} from 'react'
const ShowName = memo(function ShowName ({name}) {console.log("组件被更新")
return <div>{name}</div>
})
function App () {const [ name] = useState("张三")
const [index, setIndex] = useState(0)
useEffect(() => {setInterval (() => {setIndex(prev => prev + 1)
}, 1000)
}, [])
return <div>
{index}
<ShowName name={name}></ShowName>
</div>
}
咱们再次关上浏览器查看执行后果
当初 index 变动 子组件没有从新渲染了,用 memo 把组件变为纯组件之后就防止了依赖的值没有更新却跟着父组件一起更新的状况
4. 函数组件来实现纯组件(为 memo 办法传递自定义比拟逻辑)
memo 办法也是浅层比拟
memo 办法是有第二个参数的第二个参数是一个函数
这个函数有个两个参数,第一个参数是上一次的 props,第二个参数是下一个 props
这个函数返回 false 代表从新渲染,返回 true 从新渲染
比方咱们有员工姓名和职位两个数据,然而页面中只应用了员工姓名,那咱们只须要察看员工姓名产生变动没有,所以咱们在 memo 的第二个参数去比拟是否须要从新渲染
import {useState, useEffect, memo} from 'react'
function compare (prevProps, nextProps) {if (prevProps.person.name !== nextProps.person.name) {return false}
return true
}
const ShowName = memo(function ShowName ({person}) {console.log("组件被更新")
return <div>{person.name}</div>
}, compare)
function App () {const [ person, setPerson] = useState({name: "张三", job: "工程师"})
useEffect(() => {setInterval (() => {
setPerson({
...person,
job: "挑粪"
})
}, 1000)
}, [person])
return <div>
<ShowName person={person}></ShowName>
</div>
}
5. shouldComponentUpdata
纯组件只能进行浅层比拟,要进行深层次比拟,应用 shouldComponentUpdate,它用于编写自定义比拟逻辑
返回 true 从新渲染组件,返回 false 组件从新渲染组件
函数的第一个参数为 nextProps,第二个参数为 NextState
比方咱们有员工姓名和职位两个数据,然而页面中只应用了员工姓名,那咱们只须要察看员工姓名产生变动没有,利用 shouldComponentUpdata 来管制只有员工姓名产生变动才从新渲染组件,咱们查看应用 shouldComponentUpdata 生命周期函数和不应用 shouldComponentUpdata 生命周期函数的区别
// 没有应用的组件
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {
person: {
name: '张三',
job: '工程师'
}
}
}
componentDidMount (){setTimeout (() => {
this.setState({
person: {
...this.state.person,
job: "修水管"
}
})
}, 2000)
}
render () {console.log("render 办法执行了")
return <div>
{this.state.person.name}
</div>
}
}
咱们关上浏览器期待两秒
发现 render 办法执行了两次,组件被从新渲染了,然而咱们并没有更改 name 属性,所以这样节约了性能,咱们用 shouldComponentUpdata 生命周期函数来判断 name 是否产生了扭转
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {
person: {
name: '张三',
job: '工程师'
}
}
}
componentDidMount (){setTimeout (() => {
this.setState({
person: {
...this.state.person,
job: "修水管"
}
})
}, 2000)
}
render () {console.log("render 办法执行了")
return <div>
{this.state.person.name}
</div>
}
shouldComponentUpdate (nextProps, nextState) {if (this.state.person.name !== nextState.person.name) {return true;}
return false;
}
}
咱们再关上浏览器期待两秒之后
咱们只扭转了 job 的时候 render 办法只执行了一次,这样就缩小了没有必要的渲染,从而节约了性能
6. 应用组件懒加载
应用路由懒加载能够缩小 bundle 文件大小,从而放慢组建呈递速度
创立 Home 组建
// Home.js
function Home() {
return (
<div>
首页
</div>
)
}
export default Home
创立 List 组建
// List.js
function List() {
return (
<div>
列表页
</div>
)
}
export default List
从 react-router-dom 包中引入 BrowserRouter, Route, Switch, Link 和 home 与 list 来创立路由规定以及切换区域和跳转按钮
import {BrowserRouter, Route, Switch, Link} from 'react-router-dom'
import Home from './Home';
import List from './List';
function App () {
return <div>
<BrowserRouter>
<Link to="/"> 首页 </Link>
<Link to="/list"> 列表页 </Link>
<Switch>
<Route path="/" exact component={Home}></Route>
<Route path="/list" component={List}></Route>
</Switch>
</BrowserRouter>
</div>
}
应用 lazy, Suspense 来创立加载区域与加载函数
import {lazy, Suspense} from 'react';
import {BrowserRouter, Route, Switch, Link} from 'react-router-dom'
const Home = lazy(() => import('./Home'))
const List = lazy(() => import('./List'))
function Loading () {return <div>loading</div>}
function App () {
return <div>
<BrowserRouter>
<Link to="/"> 首页 </Link>
<Link to="/list"> 列表页 </Link>
<Switch>
<Suspense fallback={<Loading />}>
<Route path="/" exact component={Home}></Route>
<Route path="/list" component={List}></Route>
</Suspense>
</Switch>
</BrowserRouter>
</div>
}
应用注解形式来为打包后的文件命名
const Home = lazy(() => import(/* webpackChunkName: "Home" */'./Home'))
const List = lazy(() => import(/* webpackChunkName: "List" */'./List'))
7. 依据条件进行组件懒加载
实用于组件不会随条件频繁切换
import {lazy, Suspense} from 'react';
function App () {
let LazyComponent = null;
if (false){LazyComponent = lazy(() => import(/* webpackChunkName: "Home" */'./Home'))
} else {LazyComponent = lazy(() => import(/* webpackChunkName: "List" */'./List'))
}
return <div>
<Suspense fallback={<div>loading</div>}>
<LazyComponent />
</Suspense>
</div>
}
export default App;
这样就只会加载一个组件从而晋升性能
8. 通过应用占位符标记晋升 React 组件的渲染性能
React 组件中返回的 jsx 如果有多个同级元素必须要有一个独特的父级
function App () {
return (<div>
<div>1</div>
<div>2</div>
</div>)
}
为了满足这个条件咱们通常会在里面加一个 div,然而这样的话就会多出一个无意义的标记,如果每个元素都多处这样的一个无意义标记的话,浏览器渲染引擎的累赘就会加剧
为了解决这个问题,React 推出了 fragment 占位符标记,应用占位符编辑既满足了独特父级的要求,也不会渲染一个无意义的标记
import {Fragment} from 'react'
function App () {
return <Fragment>
<div>1</div>
<div>1</div>
</Fragment>
}
当然 fragment 标记还是太长了,所以有还有简写办法
function App () {
return <>
<div>1</div>
<div>1</div>
</>
}
9. 不要应用内联函数定义
在应用内联函数后,render 办法每次运行后都会创立该函数的新实例,导致 React 在进行 Virtual DOM 比照的时候,新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例,而旧的函数有要交给垃圾回收器处
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
}
render () {
return <div>
<h3>{this.state.name}</h3>
<button onClick={() => { this.setState({name: "李四"})}}> 批改 </button>
</div>
}
}
export default App;
批改为以下的形式
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
}
render () {
return <div>
<h3>{this.state.name}</h3>
<button onClick={this.setChangeName}> 批改 </button>
</div>
}
setChangeName = () => {this.setState({name: "李四"})
}
}
10. 在构造函数中进行函数 this 绑定
在类组件中如果应用 fn(){} 这种形式定义函数,函数的 this 指向默认只想 undefined,也就是说函数外部的 this 指向须要被更正,
能够在构造函数中对函数进行 this 更正,也能够在外部进行更正,两者看起来没有太大差异,然而对性能影响是不同的
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
// 这种形式应为结构器只会执行一次所以只会执行一次
this.setChangeName = this.setChangeName.bind(this)
}
render () {
return <div>
<h3>{this.state.name}</h3>
{/* 这种形式在 render 办法执行的时候就会生成新的函数实例 */}
<button onClick={this.setChangeName.bind(this)}> 批改 </button>
</div>
}
setChangeName() {this.setState({name: "李四"})
}
}
在构造函数中更正 this 指向只会更正一次,而在 render 办法中如果不更正 this 指向的话 那么就是 undefined,然而在 render 办法中更正的话 render 办法的每次执行都会返回新的函数实例这样是对性能是有所影响的
11. 类组件中的箭头函数
在类组件中应用箭头函数不会存在 this 指向问题,因为箭头函数不绑定 this
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
}
render () {
return <div>
<h3>{this.state.name}</h3>
{/* <button onClick={() => {this.setState({name: "李四"})}}> 批改 </button> */}
<button onClick={this.setChangeName}> 批改 </button>
</div>
}
setChangeName = () => {this.setState({name: "李四"})
}
}
箭头函数在 this 指向上的确比拟有劣势
然而箭头函数在类组件中作为成员应用的时候,该函数会被增加成实例对象属性,而不是原型对象属性,如果组件被屡次重用,每个组件实例都会有一个雷同的函数实例,升高了函数实例的可用性造成了资源节约
综上所述,咱们得出结论,在应用类组件的时候还是举荐在构造函数中通过应用 bind 办法更正 this 指向问题
12. 防止应用内联款式属性
当应用内联款式的时候,内联款式会被编译成 JavaScript 代码,通过 javascript 代码将款式规定映射到元素身上,浏览器就会画更多的工夫执行脚本和渲染 UI,从而减少了组件的渲染工夫
function App () {return <div style={{backgroundColor: 'red';}}></div>
}
在下面的组件中,为元素减少了背景色彩为红色,这个款式为 JavaScript 对象,背景色彩须要被转换成等效的 css 规定,而后利用到元素上,这样波及了脚本的执行,实际上内联款式的问题在于是在执行的时候为元素增加款式,而不是在编译的时候为元素增加款式
更好的形式是导入款式文件,能通过 css 间接做的事件就不要通过 JavaScript 来做,因为 JavaScript 操作 DOM 十分慢
13. 优化条件渲染以晋升组件性能
频繁的挂在和卸载组件是一件十分耗性能的事件,应该缩小组件的挂载和卸载次数,
在 React 中 咱们常常会通过不同的条件渲染不同的组件,条件渲染是一必须做的优化操作.
function App () {if (true) {
return <div>
<Component1 />
<Component2 />
<Component3 />
</div>
} else {
return <div>
<Component2 />
<Component3 />
</div>
}
}
下面的代码中条件不同的时候,React 外部在进行 Virtual DOM 比照的时候发现第一个元素和第二个元素都曾经发生变化,所以会卸载组件 1、组件 2、组件 3,而后再渲染组件 2、组件 3。实际上变动的只有组件 1,从新挂在组件 2 和组件 3 时没有必要的
function App () {if (true) {
return <div>
{true && <Component1 />}
<Component2 />
<Component3 />
</div>
}
}
这样变动的就只有组件 1 了节俭了不必要的渲染
16. 防止反复的有限渲染
当应用程序状态更改的时候,React 会调用 render 办法 如果在 render 办法中持续更改应用程序状态,就会产生递归调用导致利用报错
未捕捉谬误:超出最大更新深度。当组件在 componentWillUpdate 或 componentDidUpdate 内反复调用 setState 时,可能会产生这种状况。React 限度嵌套更新的数量以避免有限循环。React 限度的最大次数为 50 次
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {name: '张三'}
}
render () {this.setState({name:"张五"})
return <div>
<h3>{this.state.name}</h3>
<button onClick={this.setChangeName}> 批改 </button>
</div>
}
setChangeName = () => {this.setState({name: "李四"})
}
}
与其余生命周期函数不同,render 办法应该被作为纯函数,这意味着,在 render 办法中不要做以下事件
- 不要调用 setState 办法去更改状态、
- 不要应用其余伎俩查问更改 DOM 元素,以及其余更改应用程序的操作、
- 不要在 componentWillUpdate 生命周期中反复调用 setState 办法更改状态、
- 不要在 componentDidUpdate 生命周期中反复调用 setState 办法更改状态、
render 办法执行依据状态扭转执行,这样能够放弃组件的行为与渲染形式统一
15. 为组件创立谬误边界
默认状况下,组件渲染谬误会导致整个应用程序中断,创立谬误边界能够确保组件在产生谬误的时候应用程序不会中断,谬误边界是一个 React 组件,能够捕捉子级组件在渲染是产生谬误,当谬误产生时,能够记录下来,能够显示备用 UI 界面,
谬误边界波及到两个生命周期,别离是 getDerivedStateFromError 和 componentDidCatch.
getDerivedStateFromError 为静态方法,办法中须要返回一个对象,该对象会和 state 对象进行合并,用于更改应用程序状态.
componentDidCatch 办法用于记录应用程序错误信息,该办法返回的是谬误对象
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {hasError: false}
}
componentDidCatch (error) {console.log(error)
}
static getDerivedStateFromError () {
return {hasError: true}
}
render () {if (this.state.hanError) {
return <div>
产生谬误了
</div>
}
return <Test></Test>
}
}
class Test extends Component {constructor () {super()
this.state = {hanError: false}
}
render () {throw new Error("产生了谬误");
return <div>
正确的
</div>
}
}
当咱们抛出谬误的时候,getDerivedStateFromError 会合并返回的对象到 state 所以 hasError 会变成 true 就会渲染咱们备用的界面了
留神:getDerivedStateFromError 不能捕捉异步谬误,譬如按钮点击事件产生后的谬误
16. 防止数据结构渐变
组件中 props 和 state 的数据结构应该保持一致,数据结构渐变会导致输入不统一
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {
man: {
name: "张三",
age: 18
}
}
this.setMan = this.setMan.bind(this)
}
render () {const { name, age} = this.state.man
return <div>
<p>
{name}
{age}
</p>
<button onClick={this.setMan}> 批改 </button>
</div>
}
setMan () {
this.setState({
...this.state,
man: {name: "李四"}
})
}
}
乍一看这个代码貌似没有问题,认真一看咱们发现,在咱们批改了名字之后年龄字段失落了,因为数据渐变了,咱们应该去防止这样的数据渐变
import {Component} from 'react'
class App extends Component {constructor () {super()
this.state = {
man: {
name: "张三",
age: 18
}
}
this.setMan = this.setMan.bind(this)
}
render () {const { name, age} = this.state.man
return <div>
<p>
{name}
{age}
</p>
<button onClick={this.setMan}> 批改 </button>
</div>
}
setMan () {
this.setState({
man: {
...this.state.man,
name: "李四"
}
})
}
}
17. 依赖优化
在应用程序中咱们常常应用地三方的包,但咱们不想援用包中的所有代码,咱们只想用到那些代码就蕴含那些代码,此时咱们能够应用插件对依赖项进行优化
咱们应用 lodash 举例子. 利用基于 create-react-app 脚手架创立
1. 下载依赖
npm install react-app-rewired customize-cra lodash babel-plugin-lodash
react-app-rewired:笼罩 create-react-app 配置
module.exports = function (oldConfig) {return newConfig}
customize-cra:导出辅助办法,能够让以上写法更简洁
const {override, useBabelRc} = require("customize-cra")
module.exports = override((oldConfig) => newConfig,
(oldConfig) => newConfig,
)
override:能够接管多个参数,每个参数都是一个配置函数,函数承受 oldConfig,返回 newConfig
useBabelRc:容许应用.babelrc 文件进行 babel 配置
babel-plugin-lodash:对 lodash 进行精简
2. 在我的项目的根目录新建 config-overrides.js
并退出以下配置
const {override, useBabelRc} = require("customize-cra")
module.exports = override(useBabelRc())
3. 批改 package.json
文件中的构建命令
{
"script": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
}
}
4. 创立 .babelrc
文件并退出配置
{"plugins": ["lodash"]
}
5. 生产环境下的三种 JS 文件
- main.[hash].chunk.js:这是你的利用程序代码,App.js 等.
- 1.[hash].chunk.js:这是第三方库的代码,蕴含你在 node_modules 中导入的模块.
- runtime~main.[hash].js:webpack 运行时代码.
6. App 组件中代码
import _ from 'lodash'
function App () {console.log(_.chunk(['a', 'b', 'c', 'd']))
return <div>Test</div>
}
没有引入 lodash
引入 lodash
优化后的