React常见性能优化方案

4,249 阅读6分钟

图片

常用React性能优化方案


一、React.lazy进行路由切换优化

使用React.lazy动态加载组件,减少性能浪费

  • React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

使用之前:

import OtherComponent from './OtherComponent';

使用之后:

const OtherComponent = React.lazy(() => import('./OtherComponent'));
  • 使用方法: src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link} from 'react-router-dom';
import {dynamic} from './utils';
const LoadingHome = dynamic(()=>import('./components/Home'));
const LoadingUser = dynamic(()=>import('./components/User'));
ReactDOM.render(
    <Router>
        <ul>
            <li><Link to="/">Home</Link></li>
            <li> <Link to="/user">User</Link></li>
        </ul>
        <Route path="/" exact={true} component={LoadingHome}/>
        <Route path="/user" component={LoadingUser}/>
    </Router>
    ,document.getElementById('root'));

  • 核心原理: src\utils.js

const Loading = () => <div>Loading</div>;
export function dynamic(loadComponent) {
    const LazyComponent = lazy(loadComponent)
    return () => (
        <React.Suspense fallback={<Loading />}>
            <LazyComponent />
        </React.Suspense>
    )
}
function lazy(load) {
    return class extends React.Component {
        state = { Component: null }
        componentDidMount() {
            load().then(result => {
                this.setState({ Component: result.default});
            });
        }
        render() {
            let { Component } = this.state;
            return Component && <Component />;
        }
    }
}

二、使用PureComponent和memo进行更新阶段优化

  • 类组件使用PureCompnent包裹实现props的浅比较,防止props浅层不变情况下子组件的重复渲染
  • 函数式组件使用memo包裹可以达到同样效果
import React from 'react';
import {PureComponent,memo} from './utils';
export default class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {title:'计数器',number:0}
  }
  add = (amount)=>{
    this.setState({number:this.state.number+amount});
  }
  render(){
    console.log('App render');
    return (
      <div>
        <Counter number={this.state.number}/>
        <button onClick={()=>this.add(1)}>+1</button>
        <button onClick={()=>this.add(0)}>+0</button>
        <ClassTitle title={this.state.title}/>
        <FunctionTitle title={this.state.title}/>
      </div>
    )
  }
}
class Counter extends PureComponent{
  render(){
    console.log('Counter render');
    return (
     <p>{this.props.number}</p>
    )
  }
}
class ClassTitle extends PureComponent{
  render(){
    console.log('ClassTitle render');
    return (
     <p>{this.props.title}</p>
    )
  }
}
const FunctionTitle = memo(props=>{
  console.log('FunctionTitle render');
  return  <p>{props.title}</p>;
});
  • PureComponent和memo的实现原理
import React from 'react';
export class PureComponent extends React.Component{
    shouldComponentUpdate(nextProps,nextState){
        return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
    }
}
export function memo(OldComponent){
    return class extends PureComponent{
      render(){
        return <OldComponent {...this.props}/>
      }
}
}
export function shallowEqual(obj1,obj2){
    if(obj1 === obj2)
        return true;
    if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
        return false;
    }    
    let keys1 = Object.keys(obj1);
    let keys2 = Object.keys(obj2);
    if(keys1.length !== keys2.length){
        return false;
    }
    for(let key of keys1){
        if(!obj2.hasOwnProperty(key) || obj1[key]!== obj2[key]){
            return false;
        }
    }
    return true;
}

三、immutable解决memo浅比较陷阱

  • 由于memo使用的是Object.is()进行,只能比较一层(浅比较),如果props嵌套较深是无法识别到组件变化的,所以很多时候我们需要深比较,但是常规深比较涉及到大量递归算法,严重影响性能,但是借助immutable.js可以提高深比较效率。

  • immutable.js会将引用对象变成一个immutable对象,改变某一属性的时候,会更新当前属性以及它所有的父节点属性,其余属性保持不变,实现数据复用,提高深层次比较效率

import React from 'react';
import {PureComponent} from './utils';
+import { Map } from "immutable";
export default class App extends React.Component{
  constructor(props){
    super(props);
+   this.state = {count:Map({ number: 0 })}
  }
  add = (amount)=>{
+   let count = this.state.count.set('number',this.state.count.get('number') + amount);
+   this.setState({count});
  }
  render(){
    console.log('App render');
    return (
      <div>
        <Counter number={this.state.count.get('number')}/>
        <button onClick={()=>this.add(1)}>+1</button>
        <button onClick={()=>this.add(0)}>+0</button>
      </div>
    )
  }
}
class Counter extends PureComponent{
  render(){
    console.log('Counter render');
    return (
     <p>{this.props.number}</p>
    )
  }
}

四、借助react-window的FixedSizeList进行大数据量渲染优化

react-window使用示例

import React, { Component, lazy, Suspense } from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const Container = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);
ReactDOM.render(<Container/>, document.querySelector("#root"));

自定义虚拟列表

  • src\index.js
import React from 'react';
import { render } from 'react-dom';
//import VirtualList from 'react-tiny-virtual-list';
import VirtualList from './components/VirtualList';
const data = new Array(30).fill(0);

render(
    <VirtualList
        width='50%'
        height={500}
        itemCount={data.length}
        itemSize={50}
        renderItem={(data) => {
            let { index, item, style } = data;
            console.log(data);
            return (
                <div key={index} style={{ ...style, backgroundColor: index % 2 === 0 ? 'green' : 'orange' }}>
                    {index+1}
                </div>
            )
        }
        }
    />,
    document.getElementById('root')
);
  • VirtualList.js实现原理
  • src\components\VirtualList.js
import React from 'react';
export default class Index extends React.Component {
    scrollBox = React.createRef()
    state = {start: 0}
    handleScroll = () => {
        const { itemSize } = this.props;
        const { scrollTop } = this.scrollBox.current;
        const start = Math.floor(scrollTop / itemSize);
        this.setState({start})
    }
    render() {
        const { height, width, itemCount, itemSize, renderItem } = this.props;
        const { start } = this.state;
        let end = start + Math.floor(height/itemSize)+1;
        end = end>itemCount?itemCount:end;
        const visibleList = new Array(end - start).fill(0).map((item,index)=>({index:start+index}));
        const style = {position:'absolute',top:0,left:0,width:'100%', height: itemSize};
        return (
            <div
                style={{overflow: 'auto',willChange:'transform', height,width}}
                ref={this.scrollBox}
                onScroll={this.handleScroll}
            >
                <div style={{position: 'absolute',width:'100%',height: `${itemCount * itemSize}px`}}>
                   {
                        visibleList.map(({index}) => renderItem({ index, style:{...style,top:itemSize*index} }))
                    }
                </div>
            </div>
        )
    }
}

五、React hooks中使用useImmer进行性能优化

使用useMemo缓存昂贵计算

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 返回一个 memoized 值。

  • 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

使用useImmerState处理共享数据

  • 使用示例1
import React from 'react';
import ReactDOM from 'react-dom';
import {useImmerState} from './immer'
let id = 1;
function Todos() {
  const [todos, setTodos] = useImmerState({
    list: []
  })
  const addTodo = () => setTodos((draft) => {
    draft.list.push(id++)
  })
  return (
    <>
      <button onClick={addTodo}>增加</button>
      <ul>
        {
          todos.list.map((item, index) => <li key={index}>{item}</li>)
        }
      </ul>
    </>
  )
}
ReactDOM.render(
  <Todos />,
  document.getElementById('root')
);

使用useImmer库

import React from "react";
import { useImmer } from "use-immer";
 
 
function App() {
  const [person, updatePerson] = useImmer({
    name: "Michel",
    age: 33
  });
 
  function updateName(name) {
    updatePerson(draft => {
      draft.name = name;
    });
  }
 
  function becomeOlder() {
    updatePerson(draft => {
      draft.age++;
    });
  }
 
  return (
    <div className="App">
      <h1>
        Hello {person.name} ({person.age})
      </h1>
      <input
        onChange={e => {
          updateName(e.target.value);
        }}
        value={person.name}
      />
      <br />
      <button onClick={becomeOlder}>Older</button>
    </div>
  );
}

使用useRef缓存每次渲染的不变值

  • useRef可以拿来当class组件的this上下文来用,而且用useRef定义的变量更新的时候并不会更新视图层,这个特性和class组件里的this变量相同。
const countdown = useRef(0)

const countdownHandler = () => {
  if (countdown.current === 10) return
  setTimeout(() => {
		countdown.current += 1
    countdownHandler()
  }, 1000)
}

六、其他渲染方案

  • 骨架屏
  • 预渲染
  • 图片懒加载

七、关于性能优化的实践思考

  1. 并不是所有函数组件都使用memo包裹就是性能优化! 如果一个子组件过分依赖于父组件的状态,那么对于这个子组件来说使用memo包裹的意义可有可无,但是memo本身计算对比也是需要时间的。那么,如果某个子组件跟随父组件重新渲染的次数比例很大,那额外的memo对比时间就成为了负担,哪怕这个时间非常短。

  2. 不要过度依赖于useMemo useMemo本身也是有开销的,因为记忆函数本身是将依赖项数组中的依赖取出来,和上一次记录的值进行对比,如果相等才会节省本次的计算开销,否则就需要重新执行回调,这个过程本身就是消耗一定的内存和计算资源。

那么,什么时候使用useMemo,思考以下2个问题?

  • 传递给useMemo的函数开销是否大?

有些业务场景的计算开销会非常大,那么这个时候我们需要去缓存上一次的值,避免每一次父组件重新渲染就进行重新计算;如果开销并不大,那么可能useMemo本身的开销就超过了所节省的时间

  • 计算出来的值类型是否是复杂类型?

如果返回的是复杂类型(object、array),由于每次重新渲染哪怕值不变都会生成新的引用,导致子组件重新渲染,那么可以使用useMemo;如果在父组件中使用useMemo计算出来的是基本类型的值,则子组件使用memo就可以浅比较避免重新渲染,无需使用useMemo