React组件性能优化

1,051 阅读8分钟

个人学习笔记

React 组件性能优化的核心是减少渲染真实 DOM 节点的频率,减少 Virtual DOM 对比的频率

一:组件卸载前执行清理操作

  1. 在组件中为 window 组册的全局事件,以及定时器,在组件卸载前要清理掉,防止组件卸载后继续执行影响性能

二:通过纯组件提升组件性能(类组件)

什么是纯组件

  • 纯组件会对组件输入数据进行浅层比较,如果当前输入数据和上次输入数据相同,组件不会重新渲染

什么是浅层比较

  • 比较引用数据类型在内存中的引用地址是否相同,比较基本数据类型的值是否相同

使用方法

  • 类组件继承 PureComponent 类,函数组件使用 memo方法

为什么不直接进行 diff 操作,而是要先进行浅层比较,浅层比较难道没有性能消耗吗

  • 和进行diff 比较操作相比,浅层比较将消耗更少的性能。diff操作会重新遍历整颗 virtualDOM 树,而浅层比较只操作当前组件的state和props
import { Component, PureComponent } from 'react'

class APP extends Component {
	state = {
		name: '张三',
	}

	updateName = () => {
		setInterval(() => {
			this.setState({
				name: '张三',
			})
		}, 1000)
	}

	componentDidMount() {
		this.updateName()
	}

	render() {
		return (
			<div>
				<ReguarComponent name={this.state.name} />
				<PureComponentDemo name={this.state.name} />
			</div>
		)
	}
}

// 普通组件,更新内容一样也会重新渲染更新
class ReguarComponent extends Component {
	render() {
		console.log('ReguarComponent')
		return <div>{this.props.name}</div>
	}
}

// 纯组件,更新内容一样不会出现渲染更新
class PureComponentDemo extends PureComponent {
	render() {
		console.log('PureComponentDemo')
		return <div>{this.props.name}</div>
	}
}

export default APP

三:通过shouldComponentUpdate生命周期提升组件性能

  1. 纯组件只能进行浅层比较,要进行深层比较,使用 shouldComponentUpdate,它用于编写自定义比较逻辑
  2. 返回true 重新渲染组件,返回false 阻止重新渲染
  3. 函数的第一个参数为 nextProps,第二个参数为nextSate
import React, { Component } from 'react'

class App extends Component {
	state = {
        person = {
            name:'张三',
            age:20,
            job:'waiter'
        }
    }

    componentDidMount(){
        setTimeout(()=>{
            // 即使页面只展示 name age ,修改了job 也会重新渲染组件
            this.setState({
                person:{
                    ...this.state.person,job:'chef'
                }
            })
        },2000)
    }

    // name 或者 age 没有变化 组件不重新渲染
    shouldComponentUpdate(nextProps,nextState){
      if(nextState.person.name!== this.state.person.name || nextState.person.age !== this.state.person.age){
          return true
      }else {
          return false
      }
    }

	render() {
        console.log('render');
        let { name,age} = this.state
		return <div>{name} {age}</div>
	}
}

export default App

四:通过纯组件提升组件性能(函数组件)

  1. memo基本使用
  2. 将函数组件变为纯组件,将当前props和上一次的props进行浅层比较,如果相同就阻止组件重新渲染
import React, { useEffect, useState, memo } from 'react'

// 使用memo 以后父组件更新 index,子组件不会渲染更新
// 没有使用的话,父组件更新 index,子组件也会渲染更新
const ShowName = memo(({ name }) => {
	console.log('render')
	return <div>{name}</div>
})

const App = () => {
	const [name, setName] = useState('张三')
	const [index, setIndex] = useState(0)

	useEffect(() => {
		setInterval(() => {
			setIndex((prev) => prev + 1)
		}, 1000)
	}, [])

	return (
		<div>
			{index}
			<ShowName name={name} />
		</div>
	)
}

export default App

五:为memo方法传递自定义比较逻辑

import React, { useEffect, useState, memo } from 'react'

compare = (prevProps, nextProps) => {
	if (
		prevProps.person.name !== nextProps.person.name ||
		prevProps.person.age !== prevProps.person.age
	) {
		// 返回false 表示需要重新渲染,和shouldComponentUpdata相反
		return false
	} else {
		// 返回 true 表示不需要重新渲染
		return true
	}
}

// 因为memo内部只是浅层比较,此时只改变job也会让子组件重新渲染
// 传递第二个参数作为自定义比较逻辑,
const ShowPerson = memo(({ person }) => {
	console.log('render')
	return (
		<div>
			{person.name} {person.age}
		</div>
	)
}, compare)

const App = () => {
	const [person, setPerson] = useState({
		name: '张三',
		age: 20,
		job: 'waiter',
	})

	useEffect(() => {
		setInterval(() => {
			setPerson({
				...person,
				job: 'chef',
			})
		}, 1000)
	}, [])

	return (
		<div>
			<ShowPerson person={person} />
		</div>
	)
}

export default App

六:通过组件懒加载提高应用性能

  1. 使用组件懒加载可以减少 bundle 文件大小,加快组件呈递速度
import React, { lazy, Suspense } from 'react'
import { BrowserRouter, Link, Route, Switch } from 'react-router-dom'

// Suspense 和 lazy 需要配合使用  ,webpackChunkName 使用这个可以更改文件名
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

七:根据条件进行组件懒加载

  1. 适用于组件不会随条件频繁切换的场景
import React, { lazy, Suspense } from 'react'

const 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

八:通过使用占位符标记提升React组件的渲染性能

  1. 使用 Fragment 避免额外标记
  2. React 组件中返回的 jsx 如果有多个同级元素必须要有一个共同的父级
  3. 为了满足这个条件我们通常都会在最外层添加一个div,但是这样的话就会多出来一个无意义的标记,如果每个组件都多出这样的一个无意义标记的话,浏览器渲染引擎的负担就会加剧
  4. 为了解决这个问题,React推出了 fragment 占位标记,使用占位符标记既满足了拥有共同父级的要求又不回多出来额外的无意义标记
import React, { Fragment } from 'react'

// 方式一 需要引入Fragment
const APP = () => {
	return (
		<Fragment>
			<div></div>
			<div></div>
		</Fragment>
	)
}

// 方式二 不需要引入Fragment
const APP = () => {
	return (
		<>
			<div></div>
			<div></div>
		</>
	)
}

export default APP

九:通过避免使用内联函数提升组件性能

  1. 在使用内联函数后,render方法每次运行时都会创建改函数的新实例,导致 react 在进行 Virtual DOM比较时,新旧函数比对不相等,导致 react总是为元素绑定新的函数实例,而旧的函数实例又要交给垃圾回收器处理
  2. 正确的做法是在组件中单独定义函数,将函数绑定给事件
import React from 'react'

function App(props) {
	state = {
		inputValue: '',
	}

	setInputValue = (e) => {
		this.setState({
			inputValue: e.target.value,
		})
	}
	return (
		<div>
			<input
				value={this.state.inputValue}
				onChange={this.setInputValue}
			/>
		</div>
	)
}

export default App

十:在构造函数中进行this指向的更正

  1. 在类组件中如果使用 fn(){} 这种方式定义函数,函数 this 默认指向 undefined, 也就是说函数内部的 this 指向需要呗更正,可以在改造函数中对函数的 this 进行更正,也可以在行内进行更正,两者看起来没有太大区别,但是对性能的影响时不同的
import React, { Component } from 'react'

class App extends Component {
	constructor() {
		super()
		// 方式一
		// 构造函数只执行一次,所以函数 this 指向更正的代码也只执行一次
		this.handleClick = this.handleClick.bind(this)
	}

	handleClick() {
		console.log(this)
	}

	render() {
		// 方式二
		// 问题: render 方法每次执行时都会调用 bind 方法生成新的函数实例
		return (
			<div>
				<button onClick={this.handleClick.bind(this)}>按钮</button>
			</div>
		)
	}
}

export default App

十一:类组件中的箭头函数

  1. 在类组件中使用箭头函数不会存在 this 指向问题,因为箭头函数本身并不绑定 this
  2. 箭头函数在 this 指向问题上存在优势,但是同时也有不利的一面
  3. 当使用箭头函数时,该函数被添加为类的实例对象属性,而不是原型对象属性,如果组件被多次重用,每个组件实例对象中都会有一个相同的函数实例,降低了函数实例的可重用性造成了资源浪费
  4. 综上所述,更正函数内部 this 指向的最佳做法仍然是在构造函数中使用 bind 方法进行绑定

十二:避免使用内联样式属性以提升组件性能

  1. 当使用内联 style 为元素添加样式时,内联 style 会被编译为 javaScript 代码将样式规则映射到元素身上,浏览器就会花费更多的事件执行脚本和渲染 UI,从而增加了组件的渲染事件
  2. 为元素附加了内联样式,添加的内联样式为 JavaScript对象,需要被转换为等效的 CSS样式规则,然后将其应用到元素,这样涉及到脚步的执行
  3. 更好的办法是将 CSS 文件导入样式组件,能通过 CSS 直接做的事情就不要通过 javaScript 去做,因为 JavaScript操作 DOM 非常慢

十三:优化条件渲染以提升组件性能

  1. 频繁的挂载和卸载组件时一项耗性能的操作,为了确保应用程序的性能,应该减少组件挂载和卸载的次数
  2. 在 React 中我们经常会根据条件渲染不同的组件,条件渲染时一项必做的优化操作
import React from 'react'

const App = () => {
	return (
		<>
			{true && <A />}
			<B />
			<C />
		</>
	)
}

export default App

十四:避免重复的无限渲染

  1. 当应用程序状态发生更改时,React 会调用 render 方法,如果在 render 方法中继续更改应用程序状态,就会发生 render 方法递归调用导致应用程序报错
  2. 与其他生命周期函数不同,render方法应该被称为纯函数,着意味着,在 render 方法中不要做以下事情,比如不要调用setState方法,不要使用其他手段查询更改原生 DOM元素,以及其他更改应用程序的任何操作,render 方法的执行要根据状态的改变,这样可以保持组件的行为和渲染方式一致

十五:为组件创建错误边界

  1. 默认情况下,组件渲染错误会导致整个应用程序中断,创建错误边界可保持在特定组件发生错误时程序不会中断
  2. 错误边界是一个 React 组件,可以捕获子组件在渲染时发生的错误,当错误发生时,可以将错误记录下来,可以显示备用UI界面
  3. 错误边界涉及到两个生命周期函数,分别为 getDerivedStateFromError 和 componentDidCatch
  4. getDerivedStateFromError 为静态方法,方法中需要返回一个对象,改对象会和 state对象进行合并,用于更改应用程序状态
  5. componentDidCatch 方法用于记录应用程序错误信息 该方法的参数就是错误对象
import React, { Component } from 'react'

class ErrorBoundaries extends Component {
	state = {
		hasError: false,
	}
	componentDidCatch() {}

	static getDerivedStateFromError() {
		return {
			hasError: true,
		}
	}

	render() {
		if (this.state.hasError) {
			return <div>发生未知错误</div>
		}
		return <App />
	}
}

export default ErrorBoundaries

十六:避免数据结构突变

  1. 组件中 props 和 state 的数据结构应该保持一致,数据结构突变会导致输出不一致