likes
comments
collection
share

React 组件性能优化实践

作者站长头像
站长
· 阅读数 36

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 calls
setState inside componentWillUpdate or componentDidUpdate. React limits the number of
nested 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.js
import 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.js
import React from 'react';

const App = () => {
  throw new Error('报错了');
  return (
    <div>App Works</div>
  )
}

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import ErrorBoundaries from "./ErrorBoundaries";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <ErrorBoundaries/>
);