Как стать автором
Обновить

Memo в React

Время на прочтение4 мин
Количество просмотров6.3K
Автор оригинала: Dan Abramov

Много статей написано об оптимизации производительности React. В основном, если где-то обновление state происходит медленно, то вам нужно:

  1. Убедиться, что у вас запущен сборка под production. (Сборка dev умышленно медленнее, в крайнем случае - даже на порядок)

  2. Убедиться, что вы не подняли state выше по дереву, чем это необходимо. (Например, размещение state в централизованном хранилище может быть не лучшей идеей)

  3. Запустите React DevTools Profiler, чтобы увидеть, что будет заново отрисовано, оберните самые дорогие [по ресурсам] суб-деревья с помощью memo. (При необходимости добавьте useMemo)

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

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

Эти подходы дополняют то, что вы уже знаете! Они не заменяют memo или useMemo, но будет хорошее идеей начать с них.

Медленный компонент (искусственно)

Вот компонент с серьёзной проблемой производительности:

import { useState } from 'react';

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Привет, мир!</p>
      <ExpensiveTree />
    </div>
  );
}

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // искусственная задержка -- ничего не делаем 100мс
  }
  return <p>Я - очень медленное дерево компонентов.</p>;
}

(Попробуйте здесь)

Проблема в том, что каждый раз, когда цвет меняется внутри App, мы повторно рендерим <ExpensiveTree />, в которой мы искусственно добавили задержку, чтобы сделать его медленным.

Я мог бы поместить его в memo() и остановиться на этом, но уже есть много статей, поэтому я не буду тратить на это время. Я хочу показать 2 других решения.

Решение 1: перенесите state вниз

Если вы внимательно посмотрите на код рендеринга, то заметите, что только часть возвращенного дерева компонентов на самом деле заботится о цвете:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Привет, мир!</p>
      <ExpensiveTree />
    </div>
  );
}

Так что давайте извлечём эту часть в компонент Form и спустим state в этот компонент:

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}

function Form() {
  let [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

(Попробуйте здесь)

Теперь, если цвет изменится, то заново рендерится только Form. Задача решена.

Решение 2: поднимите контент вверх

Вышеупомянутой решение не работает, если часть state используется где-то над дорогим деревом. Для примера, давайте предположим, что мы поместили цвет в родительский <div>:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

(Попробуйте здесь)

Теперь кажется, что мы не можем просто извлечь нужные части, которые не используют цвет, в другой компонент, поскольку он будет включать родительский <div>, который включает <ExpensiveTree />. Теперь без memo не обойтись, не так ли?

Или все же сможем?

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

Ответ на удивление прост

Ответ
export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}

function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

(Попробуйте здесь)

Мы разделили компонент App на 2 части. Части, зависящие от цвета, вместе с самим цветом мы по поместили в ColorPicker.

Части, которые не работают с цветом, остались в компоненте App и передаются в ColorPicker как JSX-контент (children prop).

Когда цвет меняется, ColorPicker выполняет повторный рендер. Но у него всё ещё есть children prop, который получен из App в прошлый раз, поэтому React не заходит в это суб-дерево.

В результате <ExpensiveTree /> не рендерится заново.

Какова же мораль?

Прежде, чем применять оптимизацию, такую как memo или useMemo, имеет смысл посмотреть, а может вы сможете отделить части, которые меняются, от частей, которые не меняются.

Эти подходы интересны тем, что сами по себе они не имеют ничего общего с производительностью. Использование children prop для разделения компонентов обычно упрощает отслеживание потока данных в вашем приложении и сокращает количество prop, которые идут вниз. Повышение производительности в таких случаях - это как вишенка на торте, а не конечная цель.

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

Например, когда серверные компоненты готовы к внедрению, наш компонент ColorPicker может получать свои children с сервера. На сервере может работать либо весь компонент <ExpensiveTree />, либо его части, и даже изменение state верхнего уровня React "пропускает" на клиент.

Этого не сможет сделать даже memo! Но опять же, оба подхода дополняют друг друга. Не забывайте перемещать состояние вниз (а контент вверх).

Затем, если этого не хватило, то используйте Profiler и добавьте эти memo.

Я мог об этом раньше прочитать где-то ещё?

Да, возможно

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

Теги:
Хабы:
Всего голосов 11: ↑11 и ↓0+11
Комментарии6

Публикации

Истории

Работа

Ближайшие события