React Memo: Что и как использовать

Вы можете добиться молниеносной производительности приложений React различными способами, используя собственные функции и сторонние решения. React предоставляет компонент более высокого порядка под названием React.memo() , который позволяет избежать избыточного рендеринга.

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

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

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

Однако злоупотребление им может помешать нам воспользоваться его преимуществами. Более того, расточительное запоминание иногда обходится дороже, чем его игнорирование, что снижает скорость приложения.

Что такое React Memo?

React.memo — это функция, которую вы можете использовать для повышения эффективности рендеринга перехватчиков и чистых функциональных компонентов. Впервые он был доступен в React v16.6 . Памятка происходит от слова запоминание. Другими словами, если функция, завернутая в React.memo, вызывается снова с тем же вводом, она вернет кэшированную версию результата.

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

Следующие пункты описывают некоторые особенности React.memo():

  • Это компонент более высокого порядка: это функция, которая получает компонент, а затем создает другой компонент.
  • Если вы заранее знаете, что компонент вернет тот же вывод в DOM: при наличии одинаковых props вам следует обернуть его в React Memo и использовать в своем приложении React. В результате производительность будет улучшена, поскольку результат будет запомнен.
  • Использует самый последний результат рендеринга: поскольку компонент был запомнен, React не будет его рендерить и вместо этого будет использовать самый последний результат рендеринга.
  • Ищет только изменения props: это означает, что новый рендеринг будет создан только в том случае, если props будут переданы в компонент и будут изменены. Вы всегда получите запомненный результат, если props не изменились.
  • Влияние на хуки React: при изменении состояния или контекста вашего компонента, если ваш компонент использует хуки, такие как useState , useReducer или useContext , он все равно будет перерисовываться.

Где использовать React Memo?

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

Вот несколько важных моментов, на которые следует обратить внимание, где их использовать:

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

Давайте подробнее рассмотрим пункт 3 выше на примере. Это одно из лучших приложений для React Memo . Обычно в ваших приложениях React возникает ситуация, которая заставляет компонент отображаться с теми же props, что и родительский компонент. Например, родительский компонент SongsViews , показанный ниже, отображает количество просмотров песни в приложении:

function SongsViews({ title, genre, viewCount }) {
    return (
        <div>
            <Song title={title} genre={genre} />Views: {viewCount}
       </div>
);

Убедитесь, что вы заметили, что:

Мы сделали его функциональным компонентом, а не компонентом класса. Мы передали ему три props:

  • title
  • genre
  • viewCount

В практическом смысле вот как это будет отображаться:

// Initial render
<SongsViews
    viewCount={0}
    title="My Song"
    genre="Pop"
/>

// After some seconds, views is 40
<SongsViews
    viewCount={40}
    title="My Song"
    genre="Pop"
/>

// After 10 minutes, views is 250
<SongsViews
    viewCount={250}
    title="My Song"
    genre="Pop"
/>

Очевидно, что родительский компонент SongViews визуализируется каждый раз, когда свойство viewCount обновляется новым значением. При этом дочерний компонент Song также запускается для рендеринга, даже если два других свойства — название и жанр — остаются постоянными!

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

Где НЕ использовать React Memo?

Теперь вы знаете, когда использовать React Memo, поэтому давайте посмотрим, когда нам следует избегать его чрезмерного или неправильного использования. Проще говоря, вам не следует использовать React Memo во всех компонентах React, которые используют метод mustComponentUpdate() . Это связано с тем, что по умолчанию он всегда возвращает true. Метод mustComponentUpdate() , который эффективно выполняет поверхностное сравнение состояния и props, реализован в модификации рендеринга, возникающей в результате использования React Memo.

Кроме того, следует принять во внимание следующие две ситуации:

  • Если компонент легкий и в целом отображается с различными props, избегайте использования React Memo.
  • Не рекомендуется использовать React Memo для переноса компонента на основе класса, поскольку это нежелательно. Вместо этого вы можете реализовать метод mustComponentUpdate() или расширить PureComponent.

Примечание. Если компонент реагирования выдает один и тот же результат с одинаковым состоянием и props, его называют чистым компонентом.

Использование React Memo в вашем приложении

Давайте начнем создавать приложение React, использующее React Memo, теперь, когда вы полностью о нем осведомлены. Мы будем использовать React.memo , чтобы обернуть компонент для memoizing() . Давайте теперь создадим простое демонстрационное приложение, чтобы продемонстрировать, как работает мемоизация в React.

Но сначала обратите внимание на синтаксис реагирующих заметок:

const MyComponent = React.memo(function MyComponent(props) {
    /* render using props */
});

Шаг 1. Создайте новое приложение React

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

create-react-app npx react-memo-demo
react-memo-demo cd
start npm

Используя инструмент Create React App, вы загрузите новое приложение React и запустите приложение по умолчанию по адресу http://localhost:3000/ в браузере по умолчанию.

Теперь откройте файл App.js и удалите весь имеющийся код. Мы начнем с создания простого приложения со списком дел с нуля.

Шаг 2. Создайте интерфейс приложения

Наше приложение будет включать в себя:

  • Поле ввода, в котором пользователи могут ввести новый элемент задачи.
  • Кнопка, которая обрабатывает событие щелчка и добавляет в список новые элементы дел.
  • Затем идет собственно список предметов.

Вот код JSX, который нам нужно вернуть для элементов, перечисленных выше:

return (
   <div>
     <input type="text" value={text} onChange={handleText} />
     	<button type="button" onClick={handleAddTodo}>
       <span role="img" aria-label="add emojie">
         ADD +
       </span>
       Add todo
     </button>

<Todo list={todo} />
   </div>
 );

Как видите, мы также включили обязательные атрибуты value и onChange , которые позволяют полю ввода получать введенные пользователем текстовые значения и события изменения соответственно.

У нас просто есть обработчик onClick под названием handleAddTodo , прикрепленный к кнопке.

И последнее, но не менее важное: поскольку мы используем компонент под названием <Todo /> для получения элементов списка, передавая ему атрибут списка, мы не используем здесь тег неупорядоченного списка для отображения списка элементов.

Шаг 3. Создайте состояние по умолчанию, обработчики и компоненты

Хук useState () будет использоваться здесь для обработки состояния нашего приложения. Создайте функциональный компонент под названием App() , который использует useState() для инициализации нашего приложения с двумя значениями по умолчанию: список дел, покупка еды и медитация, как показано ниже:

const [todo, setTodo] = React.useState([
   { title: "food shopping 🛒" },
   { title: "Do meditation 🧘" }
 ]);

Кроме того, давайте еще раз воспользуемся useState() для инициализации текстовой переменной (которая получает фактическое значение) для поля ввода следующим образом:

const [text, setText] = React.useState("");

После этого мы получили две функции-обработчика. Давайте позаботимся о них. Первый — handleText() , который просто меняет значение в поле ввода:

const handleText = (e) => {
    setText(e.target.value);
};

Второй — handleAddTodo() , который будет использоваться для объединения значений массива и добавления его в список в качестве значения текстовой строки заголовка:

const handleAddTodo = () => {
    setTodo(todo.concat({ title: text }));
};

Компонент <Todo/> готов. Передайте ему параметр списка и верните элемент <ul> , который сопоставляется с отдельным элементом, чтобы создать новый дочерний компонент <TodoItem /> :

const Todo = ({ list }) => {
    return (
        <ul>
             {list.map((item) => (
                 <TodoItem item={item} />
             ))}
        </ul>
    );
}

Новый дочерний компонент просто вернет элемент <li> с заголовком списка, который будет отображаться после сопоставления:

const TodoItem = ({ item }) => {
    return <li>{item.title}</li>;
};

На этом этапе ваш код должен выглядеть следующим образом:

import React from "react";

const App = () => {
 const [todo, setTodo] = React.useState([
   { title: "Shop groceries 🛒" },
   { title: "Do yoga 🧘" }
 ]);

 const [text, setText] = React.useState("");

 const handleText = (e) => {
   setText(e.target.value);
 };

 const handleAddTodo = () => {
   setTodo(todo.concat({ title: text }));
 };

 return (
   <div>
     <input type="text" value={text} onChange={handleText} />
     <button type="button" onClick={handleAddTodo}>
       <span role="img" aria-label="add emojie">
         ➕
       </span>
       Add todo
     </button>

     <Todo list={todo} />
   </div>
 );
};

const Todo = ({ list }) => {
  return (
   <ul>
     {list.map((item) => (
       <TodoItem item={item} />
     ))}
   </ul>
 );
};

const TodoItem = ({ item }) => {
  return <li>{item.title}</li>;
};

export default App;

На данный момент Hooks успешно используется для создания довольно простого приложения для создания дел. Допустимо добавить 3–4 пункта. А что, если бы в нашем списке были сотни или даже тысячи пунктов? Может быть, этот список использовался нашим приложением для отображения данных, полученных из другой базы данных или API?

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

Шаг 4. Оптимизация компонентов с помощью React Memo

Теперь давайте переключим внимание на запоминание некоторых полезных элементов. Компонент <Todo/> идет первым. Оберните его в React.memo следующим образом:

// Memoized Todo component

const Todo = React.memo(({ list }) => {
    console.log("Render: Todo");
    return (
        <ul>
         {list.map((item) => (
             <TodoItem item={item} />
         ))}
        </ul>
    );
});

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

Компонент <Todo/> автоматически обновляется при изменении списка дел. Запомнив дочерний компонент <TodoItem />, как показано ниже, мы теперь можем пойти еще на один шаг и визуализировать в список только недавно добавленный элемент, а не весь список сразу.

const TodoItem = React.memo(({ item }) => {
    return <li>{item.title}</li>;
});

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

Давайте продемонстрируем влияние мемоизации, добавив несколько операторов журнала после компонентов <App /> , <Todo /> и <TodoItem /> следующим образом:

// App component
const App = () => {
 console.log("App component render");
.
.
.
// Todo component
const Todo = React.memo(({ list }) => {
 console.log("Todo component render");
.
.
.

// TodoItem component
const TodoItem = React.memo(({ item }) => {
 console.log("TodoItem component render");
.
.
.

Делая это, мы узнаем, когда каждый из них отображается, и добавляем новые элементы задач. Как мы видим, оба компонента <Todo /> и <TodoItem /> отображаются только один раз, поэтому мы использовали здесь React Memo!

Применение

Пропуск повторного рендеринга, если props не изменены

Компонент обычно повторно визуализируется React каждый раз, когда перерисовывается его родительский компонент. Memo позволяет создавать компоненты, которые React не будет повторно отображать, когда это сделает его родительский компонент, если новые props соответствуют старым props. Говорят, что такой компонент мемоизирован.

Чтобы запомнить компонент, включите его в вызов memo и замените исходный компонент возвращаемым значением:

const Greeting = memo(function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
});

export default Greeting;

Обновление мемоизированного компонента с использованием состояния

Когда его собственное состояние изменяется, компонент все равно будет повторно отображаться, даже если он был запомнен. Памяти подлежат только props, переданные компоненту от его родителя.

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

Обновление мемоизированного компонента с использованием контекста

Компонент по-прежнему будет перерисовываться при изменении контекста, который он использует, даже если он был запомнен. Мемоизации подлежат только props, переданные компоненту от его родителя.

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

Минимизация изменений props

Когда вы используете memo, каждый раз, когда props не совсем идентичен тому, что было раньше, ваш компонент перерисовывается. Это означает, что React использует сравнение Object.is для сравнения каждого свойства в вашем компоненте с его предыдущим значением. Как видите, Object.is(3, 3) возвращает true, тогда как Object.is({}, {}) возвращает false.

Уменьшите количество изменений props в заметке, чтобы получить от нее максимальную пользу. Например, если свойство является объектом, используйтеMemo :, чтобы родительский компонент не создавал его заново каждый раз.

function Page() {
  const [name, setName] = useState('ABCD');
  const [age, setAge] = useState(33);

  const person = useMemo(
    () => ({ name, age }),
    [name, age]
  );

  return <User person={person} />;
}

const User = memo(function User({ person }) {
  // ...
});

Убедитесь, что компонент принимает минимум информации в своих props, чтобы свести к минимуму обновления props. Например, он может принимать набор значений, а не весь объект:

function Page() {
  const [name, setName] = useState('ABCD');
  const [age, setAge] = useState(33);
  return <User name={name} age={age} />;
}

const User = memo(function User({ name, age }) {
  // ...
});

Указание пользовательской функции сравнения

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

Вторым аргументом memo является эта функция. Только если новые props будут давать тот же результат, что и старые props, они должны возвращать true; в противном случае он должен вернуть false.

const Chart = memo(function Chart({ dataPoints }) {
  // ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}

Пользовательская проверка равенства props

React Memo проверяет, изменилось ли свойство, переданное компоненту, между предыдущим и текущим значениями. Каждый переданный props подвергается беглому сравнению с помощью функции по умолчанию. Он проверяет, что новые значения и существующие эквивалентны.

Существующие состояния SignIn и Post сравниваются с их входящими состояниями в нашей заметке React.memo(Post) . Запомненное значение сохраняется и повторный рендеринг предотвращается, если оба значения для каждого свойства равны. Если нет, обновленное значение кэшируется, и <Post /> отображается снова.

Пользовательские компараторы:

Включив функцию сравнения в качестве второго аргумента, сравнение также можно настроить:

React.memo(Post, customComparator);

Заключение

  • React.memo — это функция, которую вы можете использовать для повышения эффективности рендеринга перехватчиков и чистых функциональных компонентов.
  • React memo — это компонент более высокого порядка.
  • Если компонент легкий и в целом отображается с различными props, избегайте использования React Memo .
  • Не рекомендуется использовать React Memo для переноса компонента на основе класса. Вместо этого вы можете реализовать метод mustComponentUpdate() .
  • Memo позволяет создавать компоненты, которые React не будет повторно отображать, когда это сделает его родительский компонент, если новые props соответствуют старым props.
  • Разделив компонент на две части, вы можете ограничить необходимость повторного рендеринга ситуациями, когда меняется только часть контекста.
  • Уменьшите количество изменений props в заметке, чтобы получить от нее максимальную пользу.