Производительность и 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 только добавит лишнюю работу по сравнению пропсов.