React组件性能提升实现方法详解
懒得跟猪打架 人气:0react组件的性能优化的核心是减少渲染真实DOM节点的频率,减少Virtual DOM比对的频率。
组件卸载前执行清理操作
在组件中为window 注册的全局事件,以及定时器,在组件卸载前要清理掉。防止组件卸载后继续执行影响应用性能。
import React from 'react' import { useEffect } from 'react' import { observer } from 'mobx-react-lite' function TestAdvance () { useEffect(() => { let timer = setInterval(() => { console.log('定时器被触发了') }, 1000) // 返回一个卸载时会被触发的函数来对timer进行清理 return () => clearInterval(timer) }, []) return <div>Test</div> } export default observer(TestAdvance)
通过纯组件提升组件性能(类组件)
什么是纯组件(PureComponent)
纯组件会对组件输入的数据进行浅层比较,如果当前输入数据和上次输入数据相同,组件不会重新渲染。
什么是浅层比较
- 比较引用数据类型在内存中的引用地址是否相同;
- 比较基本数据类型的值是否相同。
如何实现纯组件
- 类组件集成 PureComponent 类
- 函数组件使用 memo 方法
import React from 'react' class App extends React.Component { constructor () { super() this.state = { person: { name: '张三', age: 20, job: 'waiter' } } } updateName () { setInterval(() => { this.setState({ person: { ...this.state.person, name: '张三' } }) }, 3000) } componentDidMount () { console.log('componentDidMount') this.updateName() } render () { return ( <div> <RegularComponent name={this.state.person.name} /> <PureComponent name={this.state.person.name} /> </div> ) } } class RegularComponent extends React.Component { render () { console.log('RegularComponent') return <div>{this.props.name}</div> } } class PureComponent extends React.PureComponent { render () { console.log('PureComponent') return <div>{this.props.name}</div> } } export default App
浅层比较和深度diff的性能对比,为什么需要先进行浅层比较,而不直接进行diff比较呢。
和进行diff 比较相比,浅层比较将消耗更少的性能。diff 操作会重新遍历整棵 Virtual DOM树,而浅层比较只操作当前组件的 state 和 props。
可以看到PureComponent 当状态值没有改变时是不会被重新渲染的。
通过shouldComponentUpdate生命周期函数提升组件性能
shouldComponentUpdate是类组件当中的一个生命周期函数,它允许我们在这个方法当中通过返回true 或者 false,来决定是否要重新渲染组件。
纯组件只能进行浅层比较,要进行深层比较的话,需要使用到shouldComponentUpdate,它用于编写自定义比较逻辑。
函数的第一个参数时nextProps, 第二个参数是nextState。
在内存中当中有两个对象,即使这两个对象的长得一摸一样,实际上它们有不同的引用地址,这个时候再怎么比较他们都不相同。
在我们的自定义逻辑中,如果返回true,则需要重新渲染组件;如果返回false,则不需要重现渲染组件了。
import React from 'react' class App extends React.Component { constructor () { super() this.state = { person: { name: '张三', age: 20, job: 'waiter' } } } updateName () { setInterval(() => { this.setState({ person: { ...this.state.person, job: 'Writer' } }) }, 3000) } componentDidMount () { console.log('componentDidMount') this.updateName() } /* shouldComponentUpdate (nextProps, nextState) { if ( nextState.person.age !== this.state.person.age || nextState.person.name !== this.state.person.name ) { return true } else { return false } } */ render () { return ( <div> <RegularComponent name={this.state.person.name} /> <PureComponent name={this.state.person.name} /> </div> ) } } class RegularComponent extends React.Component { render () { console.log('RegularComponent') return <div>{this.props.name}</div> } shouldComponentUpdate (nextProps, nextState) { console.log(this.props, nextProps) if (nextProps.name !== this.props.name) { return true } else { return false } } } class PureComponent extends React.PureComponent { render () { console.log('PureComponent') return <div>{this.props.name}</div> } } export default App
函数组件使用memo 减少渲染次数
memo的基本使用
将函数组件变为纯组件,将当前props和上一次的props进行浅层比较,如果相同就阻止组件重新渲染。
// 未使用memo方法包裹组件 import React, { useEffect, useState } from 'react' function App () { const [name] = useState('张三') const [index, setIndex] = useState(0) useEffect(() => { let timer = setInterval(() => { setIndex(prev => prev + 1) }, 1000) return () => { clearInterval(timer) } }, [index]) return ( <div> {index} <ShowName name={name} /> </div> ) } function ShowName ({ name }) { console.log('render...') return <div>{name}</div> } export default App
使用memo封装子组件,让子组件减少不必要的渲染。
// 使用memo封装子组件 import React, { memo, useEffect, useState } from 'react' function App () { const [name] = useState('张三') const [index, setIndex] = useState(0) useEffect(() => { let timer = setInterval(() => { setIndex(prev => prev + 1) }, 1000) return () => { clearInterval(timer) } }, [index]) return ( <div> {index} <ShowName name={name} /> </div> ) } const ShowName = memo(function ({ name }) { console.log('render...') return <div>{name}</div> }) export default App
可以看到仅初次渲染时,渲染了一次。
为memo 方法传递自定义比较逻辑
在memo方法内部,其实也是进行的浅层比较。这个浅层比较对于引用数据类型来说,比较的是数据的引用地址。所以如果遇到引用数据类型的话,我们需要去传递自定义比较逻辑。
memo方法是可以接收第二个参数的,第二个参数是一个函数,我们可以通过这个函数参数来编写我们自定义比较逻辑。该函数参数接收两个参数,分别是prevProps和nextProps。
与shouldComponentUpdate的返回值逻辑相反,返回true,则不重新渲染;返回false的话则需要重新渲染。如果想让这个组件重新渲染的话就返回false,否则返回true。
// 给memo传递第二个参数,自定义比较逻辑 import React, { memo, useEffect, useState } from 'react' function App () { const [person, setPerson] = useState({ name: '张三', age: 20, job: 'waiter' }) const [index, setIndex] = useState(0) useEffect(() => { let timer = setInterval(() => { setIndex(prev => prev + 1) setPerson({ ...person, job: 'chef' }) }, 1000) return () => { clearInterval(timer) } }, [index, person]) return ( <div> {index} <ShowName person={person} /> </div> ) } function compare (prevProps, nextProps) { if ( prevProps.person.name !== nextProps.person.name || prevProps.person.age !== nextProps.person.age ) { return false } return true } const ShowName = memo(function ({ person }) { console.log('render...') return ( <div> {person.name} {person.age} </div> ) }, compare) export default App
通过组件懒加载提供应用性能
使用组件懒加载,可以减少bundle 文件大小,加快组件呈递速度。
路由组件懒加载
import React, { lazy, Suspense } from 'react' import { BrowserRouter, Routes, Route, Link } from 'react-router-dom' // import Home from './pages/Home' // import List from './pages/List' // import NotFound from './pages/NotFound' const Home = lazy(() => import(/* webpackChunkName: "Home" */ './pages/Home')) const List = lazy(() => import(/* webpackChunkName: "List" */ './pages/List')) const NotFound = lazy(() => import('./pages/NotFound')) function App () { return ( <BrowserRouter> <Link to='/'>首页 </Link> <Link to='/list'>列表页</Link> <Suspense fallback={<div>loading...</div>}> <Routes> <Route path='/' element={<Home />} /> <Route path='/list' element={<List />} errorElement={<NotFound />} /> </Routes> </Suspense> </BrowserRouter> ) } export default App
可以看到,List页面chunk 只有在页面被加载渲染时才被被请求下载。
根据条件进行组件懒加载
适用于组件不会随条件频繁切换。
import React, { lazy } from 'react' import { Suspense } from 'react' function Test () { let LazyComponent = null if (false) { LazyComponent = lazy(() => import(/* webpackChunkName: "Home-Test" */ './Home') ) } else { LazyComponent = lazy(() => import(/* webpackChunkName: "List-Test" */ './List') ) } return ( <Suspense fallback={<div>Test loading...</div>}> <LazyComponent /> </Suspense> ) } export default Test
通过使用占位符标记提升React组件的渲染性能
使用Fragment 避免额外标记
React组件中返回的jsx 如果有多个同级元素,多个同级元素必须要有一个共同的父级。
<div> ... </div> // 上面会多出一个无意义标记 // 应该改为 <fragment> ... </fragment> // 或者写成下面这样也是可以的 <> ... </>
为了满足上面的条件,我们通常都会在最外层添加一个div,但是这样的话就会多出一个无意义的标记。如果每个组件都多出这样的一个无意义标记的话,浏览器渲染引擎的负担就会加剧。
为了解决这个问题,react推出了fragment占位符标记。使用占位符标记,既满足了拥有共同的父级的要求,又不会多出额外的无意义标记。
另外,fragment标记对也可以简写成 :<></>
通过避免使用内联函数提升组件性能
因为在使用内联函数后,render 方法每次进行时都会创建该函数的新实例,导致 React 在进行Virtual DOM比对时,新旧函数比对不相等,导致总是为元素绑定新的函数实例,而旧的函数实例又要交给垃圾回收器处理。
正确的做法是:在组件中单独定义函数,将函数绑定给事件。
render(){return(<input onChange={e => this.setState({inputValue: e.target.value})} />)} // 在类组件中,应该采用下面的方式来改写从而避免该元素被重新渲染 setInputvalue = e => { this.setState({inputValue: e.target.value}) } render(){ return (<input onChange={this.setInputValue} />) }
这样一来,无论render方法被重新执行多少次,类的属性是不会发生变化的,所以在这个地方即使render方法被重新执行n次,那它每次都不会产生新的函数实例,所以它每次不会给onChange去添加新的函数。
在构造函数中进行this指向的更正
在类组件中如果使用fn(){}这种方式定义函数,函数this默认指向 undefined 。也就是说函数内部的 this 指向需要被更正。
可以在构造函数中对函数的this 进行更正,也可以在行内进行更正。两者看起来没有太大的区别,但是对性能的影响是不同的。
对于行内更正来说,每一次render方法在执行的时候它都会调用bind方法生成新的函数实例,也就是上边提到的内联函数对性能的影响是一样的。
因此比较推荐的是在构造函数当中去更正this的指向。因为构造函数只执行一次,也就是函数的this 指向只更正一次,效率较高。
import React, { Component } from 'react' export default class index extends Component { constructor () { super() // 方法一:(推荐使用) // 构造函数只执行一次,所以函数this 指向更正的代码也只执行一次。 this.handleClick = this.handleClick.bind(this) } handleClick () { console.log(this) } handleClick2= () { console.log(this) } render () { // 方式二:跟内联函数类似,不推荐,应避免使用 // 问题:render 放啊每次执行时都会调用bind方法生成新的函数实例。 return <button onClick={this.handleClick.bind(this)}>按钮</button> } }
类组件中的箭头函数
在类组件中使用箭头函数不会存在this 指向问题。因为箭头函数本身并不绑定this.
handleClick2= ()=> console.log(this)
箭头函数在this 指向问题上是比较占优势的,但是同时也有不利的一面。
当使用箭头函数时,该函数被添加为类的实例对象属性,而不是原型对象属性。如果组件被多次重用,每个组件实例对象中都会有一个相同的函数实例,降低了函数实例的可重用性,造成了资源浪费。
综上所述, 更正函数内部this 指向的最佳做法是: 在构造函数中使用 bind 方法进行绑定。 10. 避免使用内联样式属性以提升组件性能
当使用内联style 为元素添加样式时,内联style 会被编译成 JavaScript 代码,通过JavaScript代码将样式规则映射到元素的身上,浏览器就会花费更多的时间执行脚本和渲染UI,从而增加了组件的渲染时间。
// 例如:这段代码的样式会涉及到脚本的执行,效率低,资源开销大 <div style={{background: 'red'}}>Style in line</div>// 例如:这段代码的样式会涉及到脚本的执行,效率低,资源开销大 <div style={{background: 'red'}}>Style in line</div>
更好的办法是:
将CSS 文件导入样式组件,能通过CSS直接做的事情就不要通过JavaScript去做。因为JavaScript操作DOM非常慢。而CSS默认开启了GPU的渲染加速,更加高效。
优化条件渲染以提升组件性能
频繁的挂载和卸载组件,是一项耗性能的操作。为了确保应用程序的性能,应该减少组件挂载和卸载的次数。
在react中,我们经常会根据条件渲染不同的组件,条件渲染是一项必做的优化操作。
import React from 'react' import Home from './Home' import List from './List' import Test from './Test' function App () { if (true) { return ( <> <Test /> <Home /> <List /> </> ) } else { return ( <> <Home /> <List /> </> ) } } export default App
在上面的代码中,显然这种大范围的条件渲染不太合理,存在优化的空间,整个页面随着判断条件改变而变化的部分只有Test 组件,因此,可以仅对Test组件进行条件渲染判断,从而减少不必要的组件卸载和挂载的次数。
import React from 'react' import Home from './Home' import List from './List' import Test from './Test' function App () { return ( <> {true && <Test />} <Home /> <List /> </> ) } export default App
避免重复的无限渲染
当应用程序状态发生更改时,react 就会调用render 方法,如果在render 方法中继续更改应用程序状态,就会发生render 方法递归调用导致应用报错。
import React, { Component } from 'react' export default class index extends Component { constructor () { super() this.state = { name: '张三' } // 方法一:(推荐使用) // 构造函数只执行一次,所以函数this 指向更正的代码也只执行一次。 this.handleClick = this.handleClick.bind(this) } handleClick () { console.log(this) } render () { this.setState({ name: '李四' }) // 方式二:跟内联函数类似,不推荐,应避免使用 // 问题:render 放啊每次执行时都会调用bind方法生成新的函数实例。 return <button onClick={this.handleClick.bind(this)}>按钮</button> } }
与其他生命周期函数不同,render方法应该被作为纯函数。
这意味着,在render方法中不要做以下的事情:
- 不要调用setState方法;
- 不要使用其他手段查询更改原生DOM元素;
- 以及不要做其他更改应用程序的任何操作。
render 方法对的执行要根据状态的改变,这样可以保持组件的行为和渲染方式一致。
所以,在react当中,不要在render方法当中,不要在componentWillUpdate这个生命周期函数当中,不要在componentDidUpdated这个生命周期函数当中继续调用setState方法去更新状态。否则将导致重复的无限渲染,应用程序崩溃。
为应用程序创建错误边界
默认情况下,组件渲染错误会导致整个应用程序的中断,创建错误边界可确保在特定组件发生错误时应用程序不会中断。从而增加应用程序的健壮性(鲁棒性)。
错误边界是一个React组件,可以捕获子级组件在渲染时发生的错误,当错误发生时,可以将错误记录下来,可以显示备用UI界面。
错误边界涉及两个生命周期函数,分别为:getDerivedStateFromError 和 componentDidCatch.
getDerivedStateFromError: 是一个静态方法,方法中返回一个对象,该对象会和state 对象进行合并,用于更改应用程序的状态,从而给我们提供显示备用UI界面的机会。
componentDidCatch:该方法用于记录应用程序的错误信息,该方法的参数就是错误对象。
import React, { Component } from 'react' import ErrorTrigger from '../components/ErrorTrigger' export default class ErrorBoundaries extends Component { constructor () { super() this.state = { hasError: false } } componentDidCatch (error) { // 可以将程序错误信息记录到远端服务器 console.log('componentDidCatch') } static getDerivedStateFromError () { console.log('getDerivedStateFromError') return { // 该返回对象会和state 对象进行合并 hasError: true } } render () { if (this.state.hasError) { return <>我是备用UI界面</> } return ( <> <ErrorTrigger /> </> ) } }
import React, { Component } from 'react' export class ErrorTrigger extends Component { render () { throw new Error('错误边界内发生错误了') return <div>ErrorTrigger</div> } } export default ErrorTrigger
注意⚠️:错误边界不能捕获异步错误,例如点击按钮时发生的错误。
避免数据结构突变
组件中的props和state 的数据结构应该保持一致,数据结构突变会导致输出不一致。
import React, { Component } from 'react' export default class App extends Component { constructor () { super() this.state = { person: { name: '张三', age: 20, job: 'waiter' } } } render () { const { name, age, job } = this.state.person return ( <> <p> {name} {age} {job} </p> <button onClick={() => this.setState({ ...this.state, person: { age: 30 } }) } > 更新信息 </button> </> ) } }
点击更新信息按钮前
点击更新信息按钮后
这是因为数据状态在发生更改时,发生了数据结构突变导致的数据丢失。
使用setState更改状态代码应该修改为:
this.setState({ ...this.state, // 这里的person 数据结构应该和原来的保持一致,避免数据丢失 // person: { // age: 30 // } person: { ...this.state.person, age: 30 } })
修改更新数据保持结构一致的代码后点击更新信息按钮后的显示
优化依赖项大小
在程序应用开发中,常常会依赖第三方包,但我们不想引用包中所有的代码,我们只想用到哪些代码就包含哪些代码。
此时,可以使用插件对依赖项进行优化,优化资源。
拿lodash举例:
应用基于create-react-app 脚手架创建:
下载依赖
npm install react-app-rewired customize-cra lodash babel-plugin-lodash
react-app-rewired: 覆盖create-react-app的默认配置
module.exports = function(oldConfig){ return newConfig } // 参数中的oldConfig 就是默认的webpack config
customize-cra: 导出了一些辅助方法,可以让以上写法更加简洁。
const {override, useBabelRc} from 'customize-cra' module.exports = override( (oldConfig) => newConfig, (oldConfig) => newConfig ) // override 可以接收多个参数,每个参数都是一个配置函数,函数接收oldConfig,返回 newConfig // useBabelRc 允许使用 .babelrc 文件进行 babel 配置
babel-plugin-lodash: 对应用中的lodash 进行精简
在项目根目录下新建 config-overrides.js 并加入配置代码
const {override, useBabelRc} from 'customize-cra' module.exports = override(useBabelRc()) // 这里使用 .babelrc 文件进行 babel 配置
修改package.json 中的文件构建命令
把原来命令中的 react-scripts 改为 react-app-rewired
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" },
创建 .babelrc 文件并加入配置
{
"plugins":["lodash"]
}
命令打包生成的生产环境下的三种js文件
main.[hash].chunk.js 主应用程序代码,App.js 等
1.[hash].chunk.js 第三方依赖包的代码,包含在node_modules中导入的模块
runtime~main.[hash].js webpack的运行时代码
优化前后结果比对:
加载全部内容