Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Производительность и memo

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

Когда компонент перерисовывается

ПричинаПример
Изменилось его состояниеВызван setText в этом компоненте
Перерисовался родительВ App изменилось любое состояние
Изменился контекст, который он читаетПереключили тему

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

Как найти медленное место

Установите расширение браузера React Developer Tools. На вкладке Profiler нажмите запись, выполните медленное действие — например, введите текст в поле — и остановите запись. Profiler покажет, какие компоненты перерисовывались и сколько времени это заняло. Оптимизируйте только то, что действительно медленно.

Пример проблемы

В списке задач поле ввода и список лежат в одном компоненте. Каждое нажатие клавиши меняет text и перерисовывает весь TodoList:

// src/App.jsx
export default function App() {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(initialTodos);
 
  return (
    <>
      <input value={text} onChange={(event) => setText(event.target.value)} />
      <TodoList todos={todos} onDelete={handleDelete} />
    </>
  );
}

Пока задач десяток, это неважно. Если их тысячи, ввод начнёт отставать.

memo

Оберните компонент в memo — в src/components/TodoList.jsx:

// src/components/TodoList.jsx
import { memo } from 'react';
import TodoItem from './TodoItem.jsx';
 
const TodoList = memo(function TodoList({ todos, onDelete }) {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} onDelete={onDelete} />
      ))}
    </ul>
  );
});
 
export default TodoList;

Теперь React сравнивает новые пропсы со старыми и, если все они равны, пропускает перерисовку. Ввод в поле больше не трогает список.

memo и функции в пропсах

Пропсы сравниваются строго: ===. Функция handleDelete, объявленная в App, при каждой отрисовке создаётся заново, и memo увидит новый проп. Поэтому её запоминают хуком useCallback — подробно в уроке про useMemo и useCallback. То же касается объектов и массивов, созданных прямо в разметке.

Другие способы

Часто проблему решает простая перестановка: вынесите поле ввода в свой компонент AddTodoForm со своим состоянием — тогда ввод перерисует только форму, а App и список не затронет. Это лучше, чем memo, потому что не требует сравнения пропсов. Для очень длинных списков применяют виртуализацию — показ только видимых строк, например библиотекой react-window.

Частая ошибка

memo не помогает, если в пропсах каждый раз новый объект или функция:

// Неправильно: style и onDelete новые при каждой отрисовке App.
<TodoList
  todos={todos}
  style={{ margin: 8 }}
  onDelete={(id) => handleDelete(id)}
/>

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

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.