zoukankan      html  css  js  c++  java
  • React项目性能优化

    1. 使用生产版本和Fragment

    1. 生产版本

    确保发布的代码是生产模式下(压缩)打包的代码。

    一般运行npm run build命令。

    直接从webpack看配置文件,需要设置mode = 'production'。 调用teaser-webpack-plugin

    React Devtools可以根据地址栏右侧图标颜色判断是否是生产模式。

    2. Fragment

    减少不必要节点的生成。也可以使用空标签(<></>)

    2. 类组件使用PureComponent

    减少不必要的重复渲染!

    PureComponent的原理是重新设置了shouldComponentUpdate(nextProps, nextState)方法。

    在该方法中将nextProps和this.props对象浅比较,将nextState和this.state对象进行浅比较

    PureComponent模拟代码实现:

    import React from 'react';
    
    function shallowCompare(obj1, obj2) {
      if(obj1 === obj2) {
        return true;
      }
      if(typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
        return false;
      }
      const obj1Keys = Object.keys(obj1);
      const obj2Keys = Object.keys(obj2);
      if (obj1Keys.length !== obj2Keys.length) {
        return false;
      }
      for(let key of obj1Keys) {
        // 如果obj1[key]和obj2[key]为对象;浅比较有些情况会失效
        if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
          return false;
        }
      }
      return true
    }
    export default class PureComponent extends React.Component {
      shouldComponentUpdate(nextProps, nextState) {
        return !(shallowCompare(nextProps, this.props) && shallowCompare(nextState, this.state))
      }
    }

    测试用例:

    /*******父组件********/
    class App extends React.Component{
      constructor(props) {
        super(props);
        this.state = {
          number: 1
        }
        this.ref = React.createRef();
      }
      add = () => {
        // this.state.number通过prop传递给Counter
        // Counter通过PureComponnet进行浅比较
        // Number(this.ref.current.value)为0时,Counter不刷新
        this.setState(state => ({
          number: state.number + Number(this.ref.current.value)
        }))
      }
      render() {
        console.log('App Render');
        return (
          <div>
            <Counter number={this.state.number} />
            <input ref={this.ref} />
            <button onClick={this.add}>+</button>
          </div>
        )
      }
    }
    
    /*********子组件***********/
    import React from 'react';
    import PureComponent from './pureComponent';
    
    export default class Counter extends PureComponent {
      constructor(props) {
        super(props);
        this.state = {
          count: 1
        }
      }
      // 证明PureComponent会对this.state和nextState进行浅比较
      add = () => {
        // 不会导致render
        this.setState((state) => ({
          count: state.count + 0 // 0不会刷新,其他值会刷新
        }))
      }
      render() {
        console.log('Counter Render');
        return (
          <div>
            <div>
              Counter:{this.state.count}
            </div>
            <button onClick={this.add}>+</button>
            <div>
              {this.props.number}
            </div>        
          </div>
        )
      }
    };

    3. 不可变数据-immutable

    上面的PureComponent只是对简单数据(值为原始类型)进行浅比较;

    而实际开发应用中,数据都是多层嵌套的复杂数据;只使用PureComponent可能会导致render失效。

    示例(错误示例-数组):

      state= {words: []}; 
      add = () => {
        // 将一个对象赋值给另一个变量;两者对应同一个地址
        let newWords = this.state.words;
        // push,pop,shift,splice方法都不会改变原址;
        newWords.push(this.ref.current.value);
        // 如果在子组件使用了PureComponent浅比较words的值,nextProps.words === this.props.words
        // 所以不会引起子组件Counter的render方法调用
        this.setState({ 
          words: newWords
        });
      }

    上面的示例可以通过不可变数据修复BUG。

    不可变数据: 保持原数据不变,生成一个新的对象进行赋值

    示例1:

      add = () => {
        // concat方法调用后,返回一个新数组
        this.setState(state => ({
          words: state.words.concat(this.ref.current.value)
        }))
      }

    示例2:

      add = () => {
        // []符号本身就是new Array()的简写形式
        this.setState(state => ({
          words: [...state.words, this.ref.current.value]
        }))
      }

    上面的示例都是复合数据是数组的情形;对于数据是对象的示例如下:

    错误示例(对象):

    class App extends React.PureComponent{
      constructor(props) {
        super(props);
        this.state = {
          words: {}
        }
        this.ref = React.createRef();
      }
      add = () => {
        // Object.assign()被修改对象在第一个参数时,直接在原state.words对象地址上修改;
        // 对于PureComponent来说,nextState和this.state相同,永远不会render
        this.setState(state => ({
          words: Object.assign(state.words, {a:this.ref.current.value})
        }))
      }
      render() {
        console.log('App Render');
        return (
          <div>
            <input ref={this.ref} />
            <button onClick={this.add}>+</button>
          </div>
        )
      }
    }

    使用不可变数据,生成新对象的方法修改;

    示例1: 

      add = () => {
        // Object.assign()第一个参数是空对象,表明生成一个新的对象
        this.setState(state => ({
          words: Object.assign({}, state.words, {a:this.ref.current.value})
        }))
      }

    示例2:

      add = () => {
        // {}本身是new Object的简写形式
        this.setState(state => ({
          words: {...state.words, a: this.ref.current.value}
        }))
      }

    上面的方法虽然解决了使用PureComponent组件时,对象导致的不刷新问题;

    但是,会出现,只要是调用setState就重新的render的情况,尽管对应的值其实是一样的。

    因为对于js引擎来说,任何两个对象都不相同。而且,嵌套层次特别深时,书写也复杂。

    immutable库可以解决这个!

    immutable

    immutable不仅可以生成新的对象;还可以对对象进行深层次的值比较。

    import { Map, is } from 'immutable';
    
    class App extends React.Component{
      constructor(props) {
        super(props);
        this.state = {
          // is方法判断的时候,深层比较只能比较从immutable对象变化而来的对象
          // 因为这里考虑words会进行深层变化,要追踪变化,需要将其变成immutable对象
          words: Map({}),
          number: 1
        }
        this.ref = React.createRef();
      }
      shouldComponentUpdate(nextProps, nextState) {
        // is方法用于比较两个immutable对象是否值相等
        return !(is(Map(nextState), Map(this.state)) && is(Map(nextProps), Map(this.props)))
      }
      add = () => {
        // set方法给immutable对象赋值;
        // 另可以通过get获取immutable对象的值
        // this.state.words.get('a')
        this.setState(state => ({
          words: state.words.set('a', this.ref.current.value)
        }))
      }
      reset = () => {
        this.setState(state => ({
          number: state.number + 1
        }))
      }
      render() {
        console.log('App Render');
        return (
          <div>
            <input ref={this.ref} />
            <button onClick={this.add}>+</button><br/>
            <button onClick={this.reset}>reset</button>
          </div>
        )
      }
    }

    4. 函数组件使用React.memo避免重复渲染

    React.memo()本质上是将函数组件转为继承React.PureComponent的类组件的高阶组件。

    稍有不同的是,只比较props的值。

    代码实现如下:

    function memo(WrappedComponent) {
      return class extends React.PureComponent {
        render() {
          return (
            <WrappedComponent {...this.props} />
          )
        }
      }
    }

    对于深层比较,还可以接受第二个参数

    function MyComponent(props) {
      /* 使用 props 渲染 */
    }
    function areEqual(prevProps, nextProps) {
      /*
      如果把 nextProps 传入 render 方法的返回结果与
      将 prevProps 传入 render 方法的返回结果一致则返回 true,
      否则返回 false
      */
    }
    export default React.memo(MyComponent, areEqual);

    5. React.lazy()和Suspense进行懒加载

    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
    import React, { Suspense, lazy } from 'react';
    
    const Home = lazy(() => import('./routes/Home'));
    const About = lazy(() => import('./routes/About'));
    
    const App = () => (
      <Router>
        <Suspense fallback={<div>Loading...</div>}>
          <Switch>
            <Route exact path="/" component={Home}/>
            <Route path="/about" component={About}/>
          </Switch>
        </Suspense>
      </Router>
    );

    其中:React.lazy()中import只支持默认导出的组件。

    其外要包含在Suspense组件中。

    6. 异常捕获边界(Error Boundaries)

    捕获发生异常的React组件。将异常组件和正常组件分割开。

    提高用户的体验性能。

    import MyErrorBoundary from './MyErrorBoundary';
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
    
    const MyComponent = () => (
      <div>
        <MyErrorBoundary>
          <Suspense fallback={<div>Loading...</div>}>
            <section>
              <OtherComponent />
              <AnotherComponent />
            </section>
          </Suspense>
        </MyErrorBoundary>
      </div>
    );

    MyErrorBoundary代码:

    class MyErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染能够显示降级后的 UI
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        // 你同样可以将错误日志上报给服务器
        logErrorToMyService(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          // 你可以自定义降级后的 UI 并渲染
          return <h1>Something went wrong.</h1>;
        }
    
        return this.props.children; 
      }
    }

    7. 骨架屏

    骨架屏插件以react-content-loader(svg图片生层)为基础。

    用于在页面初始加载时,避免出现白屏现象。

    代码参考

    8.长列表优化

    虚拟化长列表:只加载可视范围内的数据。

    当网站需要加载大批量数据(成千上万)时,会加载特别慢。

    这个时候我们可以使用“虚拟滚动”技术(react-window或者react-virtualized),只渲染当前屏幕范围内的数据。

    鼠标滚动去触发事件,再渲染一屏。

    react-window用法示例:

    import { FixedSizeList as List } from 'react-window';
    
    const Row = ({index, style}) => (
      <div style={{...style, backgroundColor: getRandomColor()}}>
        Row{index}
      </div>
    )
    function Container(props) {
      return (
        <List
          height={200}
          itemCount={100}
          itemSize={30}
          width={'100%'}
        >
          {Row}
        </List>
      )
    }
    function getRandomColor() {
      const color = Math.floor((Math.random()*0xFFFFFF)).toString(16)
      if (color.length === 6) {
        return '#' + color
      }
      return getRandomColor();
    }
    ReactDOM.render(<Container />, window.root)

    原理是监听鼠标的滚动事件。

    实现react-window的FixedSizeList的源码如下:

    import React from 'react';
    
    export class FixedSizeList extends React.Component {
      constructor(props) {
        super(props);
        this.ref = React.createRef();
        this.state = {
          start: 0
        }
      }
      componentDidMount() {
        this.ref.current.addEventListener('scroll', () => {
          // 注意: 监听函数内部有this.ref,需要使用箭头函数
          let scrollTop = this.ref.current.scrollTop;
          this.setState({
            start: Math.floor(scrollTop/this.props.itemSize)
          })
        })
      }
      render() {
        const { itemSize, itemCount, height, width, } = this.props;
        const containerStyle = {
          height, width, position: 'relative', overflow: 'auto'
        }
        let children = [];
        const itemStyle = {
          height: itemSize,
           '100%',
          position: 'absolute',
          top: 0,
          left: 0
        }
        const { start } = this.state;
        for(let i=start; i< start + Math.ceil(height/itemSize); i++) {
          children.push(this.props.children({index: i, style: {...itemStyle, top: i*30}}))
        }
        return (
          <div style={containerStyle} ref={this.ref}>
            <div style={{ '100%', height: itemSize*itemCount}}>
              {children}
            </div>
          </div>
        )
      }
    }

    9. 根据性能优化工具修改代码

    1. 使用Chrome的Performance工具

    2. React Devtools的Profiler工具分析;

       通过React.Profiler组件包裹需要分析渲染时间的组件(不适合生产环境)。

    10. SEO优化-预渲染

    使用prerender-spa-plugin的puppeteer(无头浏览器)功能。

    11. 图片懒加载

    使用react-lazyload插件

    12. key优化

  • 相关阅读:
    JS中的逻辑或||逻辑与&&
    for 循环里面事件函数的i值
    getByClass--js
    ul 宽度不固定居中
    style.top style.left js
    trigger() 触发事件
    JavaScript中hasOwnProperty函数
    使用windows powershell ISE管理命令窗口,并集成git命令
    nodeJs跨域设置(express,koa2,eggJs)
    node获取本机动态IP,并对应修改相关JavaScript文件的IP地址
  • 原文地址:https://www.cnblogs.com/lyraLee/p/11569755.html
Copyright © 2011-2022 走看看