Published on

如何用React.memo来提高性能[翻译]

Authors
  • avatar
    Name
    祝你好运
    Twitter

即便我们不去做性能优化,React内部已经帮我们做了很多性能上的优化,而React.memo可以更进一步优化组件的渲染次数。

在这篇文章中我将会跟你解释一下如何通过使用React.memo来优化React性能,一些使用时候的常见错误,以及为什么你并不总是需要用React.memo

什么是React.memo

React.mo是React v16.6引入的一个函数,你可以用它来优化纯函数组件的渲染性能。

Memo是从memoization派生出来的,也就是记忆化的意思。简单来说就是React.memo会把被封装的函数的结果保存在内存中,对于相同输入的调用,会直接返回缓存,而不是重新计算。

这里要注意是纯函数,如果参数不变,结果就不会变。React.memo就是来防止多次重复执行的。

我写了一个简单的例子来说明这个问题,你可以在输入框中输入一些内容试一试,在这里

蓝色的组件包含三个子组件,一个输入框和两个黑块,黄色的字表示的是那个组件的渲染次数。

如果你输入一些内容的话,你会发现左边的黑块和蓝色的块都会随着你输入内容而更新,而右边的黑块就不会更新。

右边的黑块就是用React.memo来避免相同props的时候的重复渲染。

这篇文章中,我探索了React里面重新渲染的原理以及其它的优化渲染性能的方法。

现在让我们来详细看看它的原理。

如何使用React.memo

基本用法如下,你需要把你的函数组件放到React.memo里面。如果写到一行会比较好看:

const Tile = React.memo(() => {
  let eventUpdates = React.useRef(0)
  return (
    <div className="black-tile">
      <Updates updates={eventUpdates.current++} />
    </div>
  )
})

有了这个你就已经优化了你的组件的渲染速度,但是当处理更复杂的组件的时候,你有可能会遇到别的问题。

常见错误

React.memo跟想要的结果不一样?下面就是一些你可能会遇到的错误:

处理对象

假如我们想给上面的Tile加一个对象属性:

const App = () => {
  const updates = React.useRef(0)
  const [text, setText] = React.useState('')
  const data = { test: 'data' }

  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo data={data} />
      </div>
    </div>
  )
}

我们会发现TileMemo每次我们输入内容的时候都会重新渲染,不信的话你可以在codepen里面试一下。

这里React.memo结果跟我们预期不一样的原因是它只是属性的浅比较(shallow comparison),每一次App更新的时候data都会被重新声明。重新声明的data跟原来的data如果用===比较的话,是不一样的,所以TileMemo每次都会重新渲染。

1. 通过areEqual来解决

React.memo通过它的第二个参数来解决这种问题。这个参数接受一个areEqual函数,我们可以通过这个函数来控制组件何时需要渲染。

const TileMemo = React.memo(
  () => {
    let updates = React.useRef(0)
    return (
      <div className="black-tile">
        <Updates updates={updates.current++} />
      </div>
    )
  },
  (prevProps, nextProps) => {
    if (prevProps.data.test === nextProps.data.test) {
      return true // props are equal
    }
    return false // props are not equal -> update the component
  }
)

2. 通过React.useMemo来解决

你也可以用React.useMemo把对象包装起来,这样它就会记住这个对象,当App更新的时候也不会重新声明。

const data = React.useMemo(
  () => ({
    test: 'data',
  }),
  []
)

第二个参数是一个数组,数组里面是依赖的变量。如果这些变量中的任何一个有变化,React就会重新计算这个值。在我们的例子里面,我们的数组是空的,所以就不会重新计算。

处理函数

在JS里面,函数的行为和对象很像,这会导致和之前一样的问题。每次App更新的时候,onClick函数就会被声明。TileMemo会认为onClick有变化,因为引用变化了。

const App = () => {
  const updates = React.useRef(0)
  const [text, setText] = React.useState('')
  const onClick = () => {
    console.log('click')
  }

  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo onClick={onClick} />
      </div>
    </div>
  )
}

React.useCallback

我们可以通过记忆化记住这个函数来解决这个问题,就像我们处理对象那样。

const onClick = React.useCallback(() => {
  console.log('click')
}, [])

这里第二个参数也是一个数组,当数组内元素变化的时候,值也会变化。

React.memo还是不行?

如果你遇到这里没有提到的关于React.memo的问题,请给我发邮件

为什么不默认使用React.memo

你可能会认为React.memo没有任何缺点,然后想把你的所有函数组件全封装起来。这里的问题是React.memo显式的缓存了函数,也就是说它会在内存中保存结果(VDOM)。

如果你封装了太多或者太大的组件,他们也就会耗费太多内存。这就是为什么你在记忆化比较大的组件的时候要小心。

另一种你不应该使用它的情况就是属性变化太频繁。React.memo引入了额外的负担来比较属性和被记住的属性,这本身并不会影响性能太多,但是这也不是React.memo所期望的结果。

React本身在优化渲染性能方面已经非常高效,大多数时候你并不需要去优化这些不必要的重复渲染。你所要的第一件事是实际测量一下,找到性能瓶颈,可以用profile来分析一下哪个组件渲染最多,在这些地方用React.memo效果最明显。

React.memo只是理解React如何决定重新渲染组件的一部分,我会在另一篇文章中解释它,在那里我解释了为什么你不需要一开始就用React.memo

原文链接

How to use React.memo() to improve performance