Как использовать крючки мемоизации React для повышения производительности

По мере того как приложения React становятся все больше и сложнее, производительность становится все более проблематичной. Поскольку компоненты становятся больше и содержат все больше и больше подкомпонентов, рендеринг становится медленным и превращается в узкое место.

Как решить эту проблему? Если вы еще не использовали useMemo и useCallback, мы можем начать с них.

В этом уроке мы рассмотрим, как работают эти два очень простых и удобных обратных вызова, и почему они так полезны. На самом деле, в наши дни у меня болят глаза, когда я не вижу их использования. Итак, давайте погрузимся в то, что они делают.


React.useMemo

Единственная цель этого хука React — сохранить значение для последующего использования, а не пересчитывать его на месте.

Давайте рассмотрим на примере дорогостоящей логики, которая выполняется в нашей функции рендеринга:

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = list.map((i) => (i * 972 + 1000) / 5213).join(', ')

  function addRandom() {
    setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
  }

  function increaseCounter() {
    setCounter((prev) => ++prev)
  }

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это не кажется очень проблематичным, но посмотрите на переменную multiplied. Логика в этом примере не так уж плоха, но представьте себе работу с гигантским списком специальных объектов. Одно только это сопоставление может стать проблемой производительности, особенно если оно зациклено в родительском компоненте.

На этот случай существует еще один крючок состояния — counter. Когда вызывается setCounter, multiplied будет вычисляться заново, тратя предыдущие ресурсы, даже когда обновление в этом случае не требуется, так как эти переменные независимы друг от друга.

Вот тут-то и приходит на помощь useMemo (читайте официальную документацию).

Вы можете использовать этот хук, чтобы сохранить значение и получить тот же объект, пока не потребуется повторный расчет.

Вот как он используется, единственная строка, которую нам нужно изменить, это определение multiplied:

const multiplied = React.useMemo(() => {
  console.log('recalculating multiplied:', list)
  return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
Войти в полноэкранный режим Выйти из полноэкранного режима

Хук useMemo принимает 2 аргумента:

  1. Функция create — используется для возврата вычисленного значения переменной, которую мы хотим в конечном итоге использовать.
  2. Список зависимостей. Список зависимостей используется для определения того, когда должно быть вычислено новое значение — то есть, когда снова запускать функцию create.

Мы добавили сюда вызов console.log, просто чтобы отметить, когда вычисляется новое значение.

И с этими изменениями мы можем попробовать наш компонент снова (на всякий случай здесь приведен обновленный код):

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = React.useMemo(() => {
    console.log('recalculating multiplied:', list)
    return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
  }, [list])

  function addRandom() {
    setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
  }

  function increaseCounter() {
    setCounter((prev) => ++prev)
  }

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если теперь вы измените счетчик с помощью кнопки «Increase Counter», вы увидите, что наш вызов console.log не будет вызван снова, пока мы не воспользуемся другой кнопкой «Add Random».

React.useCallback

Теперь у нас есть другой хук — useCallback (читайте официальную документацию).

Он работает точно так же, как и хук useMemo — за исключением того, что он предназначен для функций, а не для значений переменных.

Мы можем взять наши функции кнопок и обернуть каждую из них в этот хук, чтобы убедиться, что наша ссылка на функцию изменяется только тогда, когда это необходимо.

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = React.useMemo(
    () => list.map((i) => (i * 972 + 1000) / 5213).join(', '),
    [list],
  )
  const addRandom = React.useCallback(
    () => setList((prev) => [...prev, Math.floor(Math.random() * 10000)]),
    [setList],
  )
  const increaseCounter = React.useCallback(() => setCounter((prev) => ++prev), [setCounter])

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь и переменные, и функции мемоизированы и будут менять ссылку только тогда, когда этого требуют их зависимости.

Предостережения

Использование этих крючков не обходится без проблем.

  1. Подумайте, действительно ли это улучшает производительность или нет в вашем конкретном случае. Если ваше состояние меняется довольно регулярно и эти мемоизации должны выполняться довольно часто, то увеличение производительности может быть перевешено затратами на выполнение логики мемоизации.

  2. Проверка и генерация зависимостей может быть дорогостоящей. Будьте осторожны с тем, что вы помещаете в списки зависимостей, и, если необходимо, сделайте еще несколько мемоизаций и отобразите ваши объекты и списки детерминированными способами, чтобы их можно было легко проверить статистически. Также избегайте использования дорогих методов, таких как JSON.stringify, для создания этих мемоизаций или зависимостей, так как это может оказаться слишком дорогим, чтобы стоило того, и может ухудшить ситуацию.

Другие моменты, которые следует учитывать

Возможно, вы захотите убедиться, что в вашем проекте используются правила lint, которые обеспечивают исчерпывающие зависимости, так как они значительно упрощают отслеживание таких вещей.

В некоторых случаях вы можете добавить комментарии игнорирования в очень специфических местах, но это дает понять, что эта часть построена таким образом намеренно, и предотвращает дальнейшую путаницу относительно того, когда обновлять зависимости.


Надеюсь, вы нашли это полезным. Есть много других хуков, о которых стоит узнать, но эти два очень полезны и часто игнорируются, поэтому я подумал, что было бы неплохо выделить их. Если вам интересно, вы можете посмотреть useRef и чем он отличается от useMemo, или, возможно, я сделаю еще одну часть об этом в будущем. Кто знает?

Оцените статью
devanswers.ru
Добавить комментарий