React 组件性能优化的外围是缩小渲染实在DOM节点的频率,缩小 VirtualDOM 比对的频率。

1.组件卸载前进行清理操作

在组件中为 window 注册的全局事件以及定时器,在组件卸载前要清理,避免组件卸载后继续执行,从而影响利用性能。

需要:开启定时器后卸载组件,查看组件中的定时器是否还在执行。

import React, {useState} from 'react';import Test from "./Test";function App() {  const [status, setStatus] = useState(true);  return (    <div className="App">      {status&&<Test/>}      <button onClick={() => setStatus(status => status =!status)}>click</button>    </div>  );}export default App;---------------------------------    import {useEffect} from "react";function Test() {  const windowBindClick = () => {    console.log('windowBindClick')  }  useEffect(() => {    let timer= setInterval(() => {      console.log('定时器在执行')    }, 1000);    window.addEventListener('click', windowBindClick);    return () => {      clearInterval(timer);      window.removeEventListener('click', windowBindClick);    }  }, []);  return <div>Test</div>}export default Test;

2.PureComponent 通过纯组件晋升组件性能

  1. 什么是纯组件
    纯组件会对组件输出数据进行浅层比拟,如果以后输出数据和上次输出数据雷同,组件不会从新渲染。
  2. 什么是浅层比拟
    比拟援用数据类型在内存中的地址是否雷同,比拟根本数据类型的值是否雷同。
  3. 如何实现纯组件
    类组件继承 PureComponent 类,函数组件应用memo办法。
  4. 为什么不间接进行diff操作,而是要先进行浅层比拟,浅层比拟难道没有耗费性能吗?
    和进行diff比拟操作相比,浅层比拟将耗费更少的性能。diff操作会从新遍历整棵virtualDOM树,而浅层比拟只操作以后组件的state和props。
  5. 需要:在状态对象中存储 name 值为张三,组件卸载实现后将 name 属性的值再次更改为张三,而后别离将 name 传递给纯组件和分纯组件,查看后果。

    // 父组件import React, {Component} from 'react';import Impurity from "./impurity";import Pure from "./pure";class App extends Component{  constructor(props) { super(props); this.state = {   name: '张三' }  }  handleSetName () { setInterval(() => {   this.setState({     name: '张三'   }) }, 1000)  }  render() { const {name} = this.state; return (   <div className="App">     <Impurity name={name}/>     <Pure name={name}/>   </div> );  }  componentDidMount() { this.handleSetName();  }}export default App;// 纯子组件import React, {PureComponent} from 'react';class Pure extends PureComponent {  constructor(props) { super(props);  }  render () { console.log('Pure render') return (   <div>{this.props.name}</div> )  }}export default Pure;// 非纯子组件import React, {Component} from 'react';class Impurity extends Component {  constructor(props) { super(props);  }  render () { console.log('Impurity render') return (   <div>{this.props.name}</div> )  }}export default Impurity;

    父组件每更新name属性为雷同值,非纯组件都会从新渲染。

3.通过 shouldComponentUpdate 申明周期函数晋升组件性能

纯组件只能进行浅层比拟,要进行深层比拟,应用 shouldComponentUpdate, 它用于编写自定义比拟逻辑。

返回 true 从新渲染组件,返回 false 组织从新渲染。

函数的第一个参数为 nextProps,第二个参数为 nextState

需要: 在页面中展现员工信息,员工信息包含:姓名、年龄、职位,但在页面中只展现姓名和年龄,也就是说只有姓名和年龄发生变化时,才有必要从新渲染组件,如何员工的其余信息产生了变动就没必要从新渲染组件。

import React, {Component} from 'react';class App extends Component {  constructor(props) {    super(props);    this.state = {      name: '张三',      age: 18,      job: 'waiter',    }  }  componentDidMount() {    setTimeout(() => {      this.setState({        ...this.state,        job: 'developers'      })    }, 2000)  }// 返回 fasle 使组件从新渲染  shouldComponentUpdate(nextProps, nextState, nextContext) {    return !this.state.name === nextState.name && this.state.age === nextState.age;  }  render () {    console.log('render...')    const {name, age} = this.state;    return <ul>      <li>{name}</li>      <li>{age}</li>    </ul>  }}export default App;

4. React.memo

1. Memo 根本应用

将函数组件变为纯组件,将以后props和上一次的props进行浅层比拟,如果雷同就阻止组件从新渲染。

需要:父组件保护两个状态,index和name,开启定时器让index一直变动,name传递给子组件,查看父组件更新的同时,子组件是否也更新了。

import React, {memo, useEffect, useState} from 'react';function App () {  const [index, setIndex] = useState(0);  const [name, setName] = useState('张三');  useEffect(() => {    const timer = setInterval(() => {      setIndex(index => index + 1);      console.log(index)    },1000)    return () => {      clearInterval(timer);    }  })  return(    <div>      <Children name={name}/>    </div>  )}// 父组件的 Index 每扭转一次,子组件都会从新渲染// function Children(props) {//   console.log('render...');//   return <div>//     <p>props.name</p>//   </div>// }// 应用memo优化子组件const Children = memo((props) => {  return (    <p>{props.name}</p>  )})export default App;

2.为memo办法传递自定义比拟逻辑

Memo 办法默认是浅层比拟,如果将一个对象传递给子组件,并更新了对象中不必要的内容,子组件还是会从新渲染,如果要达到更新不必要内容不渲染的话,须要配置 memo 办法的第二个参数,第二个参数是一个函数,在函数中能够自定义比拟逻辑,函数返回true是不更新返回false是更新,这一点与shouldComponentUpdate凑巧相同。

import React, {memo, useEffect, useState} from 'react';function App () {  const [person, setPerson] = useState({    name: '张三',    age: 18,    job: 'waiter'  });  useEffect(() => {    const timer = setInterval(() => {      setPerson(person => ({...person, job: 'java开发'}));    },1000)    return () => {      clearInterval(timer);    }  }, []);  return(    <div>      <Children person={person}/>    </div>  )}const compare = (currProps, nextProps) => {  return currProps.name === nextProps.name && currProps.age === nextProps.age;}const Children = memo((props) => {  console.log('render...');  return (    <div>      <p>{props.person.name}</p>      <p>{props.person.age}</p>    </div>  )}, compare)export default App;

5.通过组件懒加载晋升组件性能

如果不应用组件懒加载,那么所有代码都会打包进bundle文件中,那么首次加载会变的迟缓。如果是用组件懒加载,那么不同的组件会打包进不同的文件当中,能够无效的缩小bundle文件体积,从而能够让首次加载速度变得更快。

1.路由组件懒加载

import React, {lazy, Suspense} from 'react';import {BrowserRouter, Link, Route, Routes} from 'react-router-dom'; // v6// 应用lazy办法懒加载组件// /* webpackChunkName: "Home" */ 可指定打包文件名称(home.chunk.js)const Home = lazy(() => import(/* webpackChunkName: "home" */'./Home'));const List = lazy(() => import(/* webpackChunkName: "list" */'./List'));function App () {  return(    // Suspense 指定内容未加载实现时要显示的内容,能够指定组件    <Suspense fallback={<div>loading...</div>}>      <BrowserRouter>        <Link to="/">首页</Link>        <Link to="/list">列表</Link>        <Routes>            <Route path="/" element={<Home/>} exact/>            <Route path="/list" element={<List/>} exact/>        </Routes>      </BrowserRouter>    </Suspense>  )}export default App;// Home组件function Home () {  return <div>Home</div>}export default Home;// List组件function List () {  return <div>List</div>}export default List;

2.依据条件进行组件懒加载

实用于组件不会频繁切换。

import React, {lazy, Suspense, useState} from 'react';function App () {  let LazyComponent = null;  const [flag, setFlag] = useState(true);  if(flag) {    LazyComponent = lazy(() => import(/* webpackChunkName: "home" */'./Home'));  } else {    LazyComponent = lazy(() => import(/* webpackChunkName: "list" */'./List'));  }  const handleChangeComponent = () => {    setFlag(!flag)  }  return(    // Suspense 指定内容未加载实现时要显示的内容,能够指定组件    <Suspense fallback={<div>loading...</div>}>      <button onClick={handleChangeComponent}>Change Component</button>      <LazyComponent/>    </Suspense>  )}export default App;

6.应用 Fragment 防止额定标记

React 组件中返回的 jsx 如果有多个同级元素,多个同级元素必须要有一个独特的父级。

function App() {  return (      <div>        <div>text1</div>      <div>text2</div>    </div>  )}

为了满足这个条件咱们通常会在最外层增加一个div,然而这样就产生了一个无意义的标记,如果每个组件都会多出一个无意义标记的话,浏览器渲染引擎的累赘就会加剧。

为了解决这个问题,React 推出了 Fragment 占位符标记,应用了占位符标记既满足了领有独特父级的要求,又不会多出额定无意义的标记,而且这个标记并不会呈现在浏览器的dom构造中。

import react, {Fragment} from 'react';function App() {  return (      <Fragment>        <div>text1</div>      <div>text2</div>    </Fragment>  )}

还有一个种简写形式

import react from 'react';function App() {  return (      <>        <div>text1</div>      <div>text2</div>    </>  )}

7.不要应用内联函数定义

在应用内联函数后,render 办法每次运行时都会创立该函数的新实例,导致 React 在进行 virtual DOM 比对时,新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例,而旧的函数实例又要交给垃圾回收器解决。

class App extends Component {  constructor(props) {    super(props);    this.state = {      inputValue: 'text'    }  }  render() {    return <input value={this.state.inputValue} onChange={e => this.setState({inputValue: e.target.value})}/>  }}

正确的做法应该是在组件中独自定义函数,将函数绑定给事件。

// 类组件class App extends Component {  constructor(props) {    super(props);    this.state = {      inputValue: 'text'    }  }      // 定义为类属性办法  setInputValue = e => {    this.setState({inputValue: e.target.value })  }  render() {    return <input value={this.state.inputValue} onChange={this.setInputValue}/>  }}// 函数组件import React, {useState, useCallback} from 'react';function App {      const [inputValue, setInputValue] = useState('');      setInputValue = useCallback(e => {    setInputValue(inputValue = > e.target.value);  }, [setInputValue])  render() {    return <input value={this.state.inputValue} onChange={setInputValue}/>  }}

8.在构造函数中进行函数this绑定

在类组件中如果应用 fn() {} 这种形式定义函数,函数 this 默认指向 undefined ,也就是说函数外部 this 指向须要被更正。

能够在构造函数中对函数的this进行更正,也能够在行内进行更正,两个看起来没有太大区别,然而对性能影响是不同的。

class App extends Component {  constructor(props) {    super(props);    // 构造函数只会执行一次,所以更正this的代码也只执行一次    this.handleClick = this.handleClick.bind(this);  }  handleClick () {    console.log(this)  }  render() {    // render 每次渲染都会执行,所以每次调用都会生成新的函数实例    return <button onClick={this.handleClick.bind(this)}>click</button>  }}

9.类组件中的箭头函数

类组件中应用箭头函数不会存在this指向问题,因为箭头函数本身并不绑定 this

class App extends Component {  handleClick = () => {    console.log(this)  }  render() {    return <button onClick={this.handleClick}>click</button>  }}

箭头函数在 this 指向问题上占据劣势,但同时也有不利的一面。

当应用箭头函数时,该函数被增加为类的实例对象属性,而不是原型对象属性,如果组件被屡次重用,每个组件实例对象都将会有一个雷同的函数实例,升高了函数的可重用性,造成了资源节约。

综上所述,更正函数外部 this 指向的最佳做法仍是在构造函数中应用 bind 办法进行绑定。

10.防止应用内联款式属性

当应用内联 style 为元素增加款式时,内联 style 会被编译为 JavaScript 代码,通过 JavaScript 代码将款式规定映射到元素身上,浏览器就会破费更多的工夫执行脚本和渲染UI,从而减少了组件的渲染工夫。

function App() {  return <div style={{backgroundColor: "cadetblue"}}>app works</div>}

在下面的组件中,为元素附加了内联款式,增加的内联款式为JavaScript对象,backgroundColor 须要被转换为等效的 CSS 款式规定,而且是在执行时而非编译,而后将其利用到元素,这样波及到脚本执行。

更好的方法是将 CSS 文件导入款式组件,能通过 CSS 间接做的事件尽量不要通过 JavaScript 去做,因为 JavaScript 操作 DOM 很慢。

11.优化条件渲染

频繁的挂载和卸载组件是一项耗性能的操作,为了确保应用程序的性能,应该缩小组件挂载和卸载的次数。

在React中咱们常常会依据条件渲染不同的组件,条件渲染是一项必做的优化操作。

function App () {  if(true) {    return (        <>          <AdminHeader/>          <Header/>          <Content/>      </>    )  } else {    <>        <Header/>        <Content/>    </>  }}

在下面的代码中, 当渲染条件发生变化时, React 外部在做 Virtual DOM 比对时发现, 刚刚第一个组件是 AdminHeader, 当初第一个组件是Header, 刚刚第二个组件是 Header, 当初第二个组件是 Content, 组件产生了变动, React 就会卸载 AdminHeader、Header、Content, 从新挂载 Header 和Content, 这种挂载和卸载就是没有必要的.

function App () {    return (        <>          {true && <AdminHeader/>}          <Header/>          <Content/>      </>    )}

12.防止反复有限渲染

当应用程序状态产生更改时, React 会调用 render 办法, 如果在 render 办法中持续更改应用程序状态, 就会产生 render 办法递归调用导致利用报错。

Error: Maximum update depth exceeded. This can happen when a component repeatedly callssetState inside componentWillUpdate or componentDidUpdate. React limits the number ofnested updates to prevent infinite loops.
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 办法的执行要依据状态的扭转,这样能够放弃组件的行为和渲染形式统一。

13. 为组件创立谬误边界

默认状况下, 组件渲染谬误会导致整个应用程序中断, 创立谬误边界可确保在特定组件产生谬误时应用程序不会中断.
谬误边界是一个 React 组件, 能够捕捉子级组件在渲染时产生的谬误, 当谬误产生时, 能够将谬误记录下来, 能够显示备用 UI 界面,谬误边界波及到两个生命周期函数, 别离为 getDerivedStateFromError 和 componentDidCatch.
getDerivedStateFromError 为静态方法, 办法中须要返回一个对象, 该对象会和state对象进行合并, 用于更改应用程序状态.
componentDidCatch 办法用于记录应用程序错误信息. 该办法的参数就是谬误对象.

// ErrorBoundaries.jsimport React, {Component} from 'react';import App from "./App";class ErrorBoundaries extends Component {  constructor(props) {    super(props);    this.state = {      hasError: false,    }  }  componentDidCatch(error, errorInfo) {    console.log(error, errorInfo);  }  static getDerivedStateFromError () {    console.log('getDerivedStateFromError');    return {      hasError: true    }  }  handleRunError = () => {    this.setState({      hasError: true    })  }  render() {    if(this.state.hasError) {      return <div>产生了未知谬误</div>    }    return <App/>  }}export default ErrorBoundaries;
// App.jsimport React from 'react';const App = () => {  throw new Error('报错了');  return (    <div>App Works</div>  )}export default App;
// index.jsimport React from 'react';import ReactDOM from 'react-dom/client';import ErrorBoundaries from "./ErrorBoundaries";const root = ReactDOM.createRoot(document.getElementById('root'));root.render(    <ErrorBoundaries/>);