react 性能优化

162 阅读7分钟

react 性能优化

useCallback的正确使用

1.useCallback 配合 React.memo

官网: useCallback该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

父组件的每次状态更新,都会导致子组件重新渲染,即使传入子组件的props没有变化,为了减少子组件重复渲染,我们可以使用React.memo来缓存组件,这样只有当传入组件的状态值发生变化时才会重新渲染,如果传入相同的值,则返回缓存的组件。另一方面如果父组件给子组件传入一个callback回调函数,父组件每次render,函数都会执行,这时我们可以使用useCallback缓存函数,只有依赖项发生变更才会执行函数。

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

React.memo(Component, [areEqual(prevProps, nextProps)]); 第一个参数是需要缓存的组件,第二个参数是一个函数可以自定义比较逻辑,该函数有2个参数,第一个参数是上一次的props,第二个参数是下一个props,eg:

function moviePropsAreEqual(prevMovie, nextMovie) {
     return prevMovie.title === nextMovie.title 
            && prevMovie.releaseDate === nextMovie.releaseDate;
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);

// 使用React.memo缓存子组件,父组件使用useCallback缓存函数,则只有useCallback依赖的state值改变,子组件才会render
const Button = React.memo((props)=>{
    console.log('Button组件render了')
    return (
        <button onClick={props.handleClick}>{props.children}</button>
    )
})


function Counter() {
    const [count, setCount] = useState(0);
    const [value,setValue] = useState('')

    // 没有使用`useCallback`,每次渲染都会重新创建内部函数
    // const handleClick =()=>{
    //     console.log('handleClick');
    //     setCount(count+1);
    // }

   // 使用useCallback,只有当依赖项变更时才会创建内部函数   
    const handleClick = useCallback(()=>{
        console.log('handleClick');
        setCount(count+1);
    },[count])

    return (
        <div>
            <p>{count}</p>
            <Button>Click</Button>
            <input 
              type="text" 
              value={value} 
              onChange={(e)=>setValue(e.target.value)}/>
        </div>
    )
}

再看看下面这个更复杂的例子吧:

// 注意:ExpensiveTree 比较耗时记得使用`React.memo`优化下,要不然父组件优化也没用
const ExpensiveTree = React.memo(function (props) {
    console.log('Render ExpensiveTree')
    const { onClick } = props;
    const dateBegin = Date.now();
    // 很重的组件,不优化会死的那种,真的会死人
    while(Date.now() - dateBegin < 600) {}

    useEffect(() => {
        console.log('Render ExpensiveTree --- DONE')
    })

    return (
        <div onClick={onClick}>
            <p>很重的组件,不优化会死的那种</p>
        </div>
    )
});

 function Index() {
    const [text, updateText] = useState('');
    const handleSubmit = useCallback(() => {
        console.log(`Text: ${text}`);
    }, [text]);

    return (
        <div>
            <input value={text} onChange={(e) => updateText(e.target.value)} />
            <ExpensiveTree onClick={handleSubmit} />
        </div>
    )
}

问题:

  1. 更新input值,发现比较卡顿。

继续优化 useRef解决方案

优化的思路:

  1. 为了避免子组件ExpensiveTree在无效的重新渲染,必须保证父组件re-render时handleSubmit属性值不变;
  2. handleSubmit属性值不变的情况下,也要保证其能够访问到最新的state。
export default function Index() {
    const [text, updateText] = useState('Initial value');
    const textRef = useRef(text);

    const handleSubmit = useCallback(() => {
        console.log(`Text: ${textRef.current}`);
    }, [textRef]);

    useEffect(() => {
        console.log('update text')
        textRef.current = text;
    }, [text])

    return (
        <>
            <input value={text} onChange={(e) => updateText(e.target.value)} />
            <ExpensiveTree onClick={handleSubmit} />
        </>
    )
}

原理:

  1. handleSubmit由原来直接依赖text变成了依赖textRef,因为每次re-render时textRef不变,所以handleSubmit不变;
  2. 每次text更新时都更新textRef.current。这样虽然handleSubmit不变,但是通过textRef也是能够访问最新的值。

useRef+useEffect这种解决方式可以形成一种固定的模式(useEventCallback):

export default function Index() {
   const [text, updateText] = useState('Initial value');

   const handleSubmit = useEventCallback(() => {
       console.log(`Text: ${text}`);
   }, [text]);

   return (
       <>
           <input value={text} onChange={(e) => updateText(e.target.value)} />
           <ExpensiveTree onClick={handleSubmit} />
       </>
   )
}

function useEventCallback(fn, dependencies) {
   const ref = useRef(null);

   useEffect(() => {
       ref.current = fn;
   }, [fn, ...dependencies])

   return useCallback(() => {
       ref.current && ref.current(); // 通过ref.current访问最新的回调函数 fix 
   }, [ref])
}
  1. 通过useRef保持变化的值,
  2. 通过useEffect更新变化的值;
  3. 通过useCallback返回固定的callback。

useEventCallback里没必要增加deps(判断deps是否变化也消耗性能)。useEventCallback里本质上就是更新ref.current,每次re-render都更新得了:

fix

function useEventCallback(fn) {
    const ref = useRef(fn);

    useEffect(() => {
        ref.current = fn;
    })

    return useCallback(args => {
        return ref.current && ref.current.apply(void 0, args); // 通过ref.current访问最新的回调函数
    }, [ref]) // 这里也可以不写依赖ref
}

2.组件卸载前清理副作用

3.使用组件懒加载

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

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


function App () {
  return (
     <div>
        <div>1</div>
        <div>2</div>
    </div>
   )
}

为了满足这个条件我们通常会在外面加一个div,但是这样的话就会多出一个无意义的标记,如果每个元素都多处这样的一个无意义标记的话,浏览器渲染引擎的负担就会加剧

为了解决这个问题,React 推出了 fragment 占位符标记,使用占位符编辑既满足了共同父级的要求,也不会渲染一个无意义的标记

import { Fragment } from 'react'
function App () {
  return(
         <Fragment>
                <div>1</div>
                <div>1</div>
          </Fragment>
       )
}

当然 fragment 标记还是太长了,所以有还有简写方法

function App () {
  return (
        <>
            <div>1</div>
            <div>1</div>
        </>
     )
}

5. 不要使用内联函数定义

在使用内联函数后,render 方法每次运行后都会创建该函数的新实例,导致 React 在进行 Virtual DOM 对比的时候,新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例,而旧的函数有要交给垃圾回收器处

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={() => { this.setState({name: "李四"})}}>修改</button>
    </div>
  }
}


export default App;

修改为以下的方式

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }

}

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

在类组件中如果使用 fn(){} 这种方式定义函数,函数的 this 指向默认只想 undefined,也就是说函数内部的 this 指向需要被更正,

可以在构造函数中对函数进行 this 更正,也可以在内部进行更正,两者看起来没有太大差别,但是对性能影响是不同的

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
    // 这种方式应为构造器只会执行一次所以只会执行一次
    this.setChangeName = this.setChangeName.bind(this)
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* 这种方式在render方法执行的时候就会生成新的函数实例 */}
      <button onClick={this.setChangeName.bind(this)}>修改</button>
    </div>
  }
  setChangeName() {
    this.setState({name: "李四"})
  }
}

在构造函数中更正this指向只会更正一次,而在render方法中如果不更正this指向的话 那么就是 undefined ,但是在render方法中更正的话render方法的每次执行都会返回新的函数实例这样是对性能是有所影响的

7. 类组件中的箭头函数

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

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* <button onClick={() => { this.setState({name: "李四"})}}>修改  </button> */}
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
}

箭头函数在this指向上确实比较有优势

但是箭头函数在类组件中作为成员使用的时候,该函数会被添加成实例对象属性,而不是原型对象属性,如果组件被多次重用,每个组件实例都会有一个相同的函数实例,降低了函数实例的可用性造成了资源浪费

综上所述,我们得出结论,在使用类组件的时候还是推荐在构造函数中通过使用bind方法更正this指向问题

8. 避免使用内联样式属性

当使用内联样式的时候,内联样式会被编译成JavaScript代码,通过javascript代码将样式规则映射到元素身上,浏览器就会画更多的时间执行脚本和渲染UI,从而增加了组件的渲染时间

function App () {
  return <div style={{backgroundColor: 'red';}}></div>
}

在上面的组件中,为元素增加了背景颜色为红色,这个样式为JavaScript对象,背景颜色需要被转换成等效的css规则,然后应用到元素上,这样涉及了脚本的执行,实际上内联样式的问题在于是在执行的时候为元素添加样式,而不是在编译的时候为元素添加样式

更好的方式是导入样式文件,能通过css直接做的事情就不要通过JavaScript来做,因为JavaScript操作 DOM 非常慢

9. 优化条件渲染以提升组件性能

频繁的挂载和卸载组件是一件非常耗性能的事情,应该减少组件的挂载和卸载次数,

在React中 我们经常会通过不同的条件渲染不同的组件,条件渲染是一必须做的优化操作.

function App () {
  if (true) {
    return <div>
      <Component1 />
        <Component2 />
      <Component3 />
    </div>
  } else {
    return <div>
        <Component2 />
        <Component3 />
    </div>
  }
  
}

上面的代码中条件不同的时候,React 内部在进行Virtual DOM 对比的时候发现第一个元素和第二个元素都已经发生变化,所以会卸载组件1、组件2、组件3,然后再渲染组件2、组件3。实际上变化的只有组件1,重新挂在组件2和组件3时没有必要的

function App () {
  if (true) {
    return <div>
      { true && <Component1 />}
        <Component2 />
      <Component3 />
    </div>
  }
}

这样变化的就只有组件1了节省了不必要的渲染

10. 为组件创建错误边界