Главная / Статьи

Почему React.memo продолжает перерисовку: идентичность ссылки в пропсах

Узнайте, как React.memo сравнивает атрибуты, почему встроенные функции, объекты и массивы мешают ему работать, как стабилизировать ссылки и почему профилирование должно предшествовать мемоизации.

Издатель: REACTAPP.TOP5 мин чтения1092 слов

Источники: React.memo: Useful, but Often Misused

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

Что на самом деле проверяет memo

memo не препятствует отрисовке компонента. Он сравнивает новые параметры с предыдущими, по одному за разом, с помощью Object.is, и пропускает отрисовку только тогда, когда все они совпадают. Изменения в собственном состоянии компонента или в контексте, который он читает, всё равно приводят к его отрисовке. Примитивные параметры, такие как строки, числа и логические значения, сравниваются по значению, поэтому они работают естественным образом.

import { memo } from "react";

const App = () => {
  const name = "John";
  const age = 25;

  return <UserCard name={name} age={age} />
};

const UserCard = memo(({ name, age }) => {
  return (
    <div>
      <h3>{name}</h3>
      <p>Age: {age}</p>
    </div>
  );
});

Если name и age не изменились, React может повторно использовать предыдущий результат. Проблемы возникают с функциями, объектами и массивами, поскольку для них Object.is сравнивает ссылки, а не содержимое.

Встроенные обратные вызовы генерируются заново при каждой отрисовке

Здесь родительский компонент передаёт функцию-стрелку в мемизированную кнопку.

import React, { useState, memo } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Count: {count}</h2>

      <CounterButton
        onIncrement={() => setCount((prev) => prev + 1)}
      />
    </div>
  );
};

const CounterButton = memo(({ onIncrement }) => {
  return (
    <button onClick={onIncrement}>
      Increment
    </button>
  );
});

При каждой отрисовке компонента App создается новая функция. Её тело остаётся идентичным, но ссылка на неё меняется, поэтому механизм memo считает, что свойство изменилось, и каждый раз отрисовывает компонент CounterButton.

Решение заключается в том, чтобы сделать ссылку стабильной с помощью useCallback. Поскольку используется форма обновления prev => prev + 1, массив зависимостей может оставаться пустым.

import React, { useState, useCallback } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <div>
      <h2>Count: {count}</h2>

      <CounterButton
        onIncrement={handleIncrement}
      />
    </div>
  );
};

const CounterButton = ({ onIncrement }) => {
  return (
    <button onClick={onIncrement}>
      Increment
    </button>
  );
};

Обратите внимание, что в этом фрагменте компонент CounterButton больше не находится внутри механизма memo. Без этого обёртки дочерний компонент будет перерисовываться вместе со своим родителем независимо от обстоятельств, и использование useCallback не принесёт никакой пользы. Оба элемента эффективны только вместе: необходимо сохранить обёртку memo(...) вокруг дочернего компонента из предыдущего примера. Также не стоит добавлять useCallback везде; у него есть свои издержки, и он должен использоваться там, где решает конкретную проблему.

Объекты встроенного типа и массивы ведут себя одинаково

Литерал объекта внутри тела компонента создается заново при каждой отрисовке.

import { memo } from "react";

const App = () => {

  const config = {
    theme: "dark",
    pageSize: 20,
    showFilters: true,
  };

  return (
    <div>
      <DataGrid config={config} />
    </div>
  );
};

const DataGrid = memo(({ config }) => {
  return (
    <div>
      <h3>Theme: {config.theme}</h3>
      <p>Page Size: {config.pageSize}</p>
    </div>
  );
});

То же самое касается и массива опций.

import { memo } from "react";

const App = () => {
  const options = [
    { label: "Admin", value: "admin" },
    { label: "Editor", value: "editor" },
    { label: "User", value: "user" },
  ];

  return (
    <div>
      <Select options={options} />
    </div>
  );
};

const Select = memo(({ options }) => {

  return (
    <select>
      {options.map((option) => (
        <option key={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
});

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

Переместите данные, которые никогда не меняются

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

import { useState, useMemo, memo } from "react";

const options = [
      { label: role.toUpperCase(), value: role },
      { label: "Editor", value: "editor" },
      { label: "User", value: "user" },
    ]

const App = () => {

  return (
    <div>
      <Select options={options} />
    </div>
  );
};

const Select = memo(({ options }) => {
  return (
    <select>
      {options.map((option) => (
        <option key={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
});

export default App;

Как показано, первый элемент ссылается на переменную role, которой нет в области видимости модуля, поэтому этот код вызывает ошибку ReferenceError. Используйте литерал вроде { label: "Admin", value: "admin" }. Если элемент действительно зависит от параметров или состояния, храните массив внутри компонента и оберните его функцией useMemo, указав эти значения в качестве зависимостей (вероятно, именно поэтому импортируется useMemo).

Объект конфигурации обрабатывается аналогичным образом.

import { memo } from "react";

const config = {
  theme: "dark",
  pageSize: 20,
  showFilters: true,
};

const App = () => {
  return (
    <div>
      <DataGrid config={config} />
    </div>
  );
};

const DataGrid = memo(({ config }) => {

  return (
    <div>
      <h3>Theme: {config.theme}</h3>
      <p>Page Size: {config.pageSize}</p>
    </div>
  );
});

Измеряйте перед тем, как использовать memoization

Перед рассмотрением любых механизмов оптимизации важно сначала ответить на ключевые вопросы: какой именно компонент работает медленно, почему происходит его отрисовка, является ли эта процедура ресурсоемкой, и можно ли уменьшить объем работы вместо того, чтобы её скрывать. Инструмент профилирования React DevTools отвечает на большинство из этих вопросов, показывая, какие компоненты были отрисованы, почему, сколько времени это заняло и какие обновления были ненужны. Чек-лист по сокращению объема работы перед применением механизма мемоизации дополняет этот порядок действий.

Основные выводы

  • memo эффективен только тогда, когда каждый параметр сохраняет стабильную ссылку.
  • Встроенные функции, объекты, массивы и значения контекста часто сводят его эффективность на нет.
  • Использование useCallback без мемоизированного потомка — это пустая трата усилий.
  • Используйте useMemo для ресурсоемких вычислений или в случаях, когда ссылки должны оставаться стабильными.
  • Виртуализация длинных списков и разделение крупных компонентов обычно приносит больше преимуществ, чем использование мемоизации.
  • Сначала проведите профилирование; пусть данные сами решат.