JavaRush /Курсы /Модуль 3: React /Оптимизация производительности с React.memo и useCallback...

Оптимизация производительности с React.memo и useCallback

Модуль 3: React
19 уровень , 7 лекция
Открыта

React.memo: мемоизация компонентов

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

Сегодня мы рассмотрим два инструмента оптимизации:

  • React.memo — для предотвращения лишних рендеров компонентов;
  • useCallback — для мемоизации функций, чтобы не передавать их заново при каждом рендере.

Они особенно полезны, если в вашем приложении:

  • Они особенно полезны, если в вашем приложении:
  • есть часто обновляемое состояние (например, форма ввода);
  • много взаимодействий между родительскими и дочерними компонентами.

React.memo: оптимизация компонентов

React.memo — это обёртка, которая запоминает результат рендера компонента и использует его повторно, если пропсы не изменились.

Это особенно полезно в списках: если у нас 100 транзакций, и изменяется только одна — нет смысла перерисовывать остальные 99.

Пример: компонент транзакции

Файл src/components/TransactionItem.tsx

import React from "react";

interface TransactionItemProps {
  category: string;
  amount: number;
  date: string;
  type: "income" | "expense";
}

const TransactionItem: React.FC<TransactionItemProps> = ({ category, amount, date, type }) => {
  console.log(`Рендерится транзакция: ${category}`);

  return (
    <li>
      {date.slice(0, 10)} — {category}: {type === "income" ? "+" : "-"}${amount}
    </li>
  );
};

// Оборачиваем в React.memo
export default React.memo(TransactionItem);

Теперь этот компонент будет перерендериваться только при изменении его пропсов.

useCallback: мемоизация функций

Функции в JavaScript — это объекты. Поэтому при каждом рендере создаётся новая функция, даже если она выглядит одинаково. Это мешает React.memo, потому что "старый" и "новый" пропс выглядят разными.

Пример без useCallback

const Parent = () => {
  const handleClick = () => {
    console.log("Нажато");
  };

  return <Child onClick={handleClick} />;
};

Каждый раз при рендере Parent, handleClick создаётся заново — а значит, Child думает, что пропсы изменились и тоже перерендеривается.

Решение: useCallback

Пример с useCallback

import React, { useCallback } from "react";

const Parent: React.FC = () => {
  const handleClick = useCallback(() => {
    console.log("Нажато");
  }, []); // Функция не изменится, пока зависимости не изменятся

  return <Child onClick={handleClick} />;
};

const Child: React.FC<{ onClick: () => void }> = React.memo(({ onClick }) => {
  console.log("Рендерится Child");
  return <button onClick={onClick}>Нажми меня</button>;
});

Теперь handleClick не пересоздаётся при каждом рендере, а Child не перерендеривается без причины.

Применение в проекте: список транзакций

Представим, что у нас есть список транзакций и кнопка "Удалить". Нам важно, чтобы при добавлении одной транзакции остальные не перерисовывались.

Файл src/components/TransactionItem.tsx

import React from "react";

interface Transaction {
  id: number;
  category: string;
  amount: number;
  date: string;
  type: "income" | "expense";
  onDelete: (id: number) => void;
}

const TransactionItem: React.FC<Transaction> = ({ id, category, amount, date, type, onDelete }) => {
  console.log(`Рендер: ${category}`);

  return (
    <li>
      {date.slice(0, 10)} — {category}: {type === "income" ? "+" : "-"}${amount}
      <button onClick={() => onDelete(id)}>Удалить</button>
    </li>
  );
};

export default React.memo(TransactionItem);

Файл src/components/TransactionList.tsx

import React, { useCallback } from "react";
import TransactionItem from "./TransactionItem";

interface Transaction {
  id: number;
  category: string;
  amount: number;
  date: string;
  type: "income" | "expense";
}

interface Props {
  transactions: Transaction[];
  onDelete: (id: number) => void;
}

const TransactionList: React.FC<Props> = ({ transactions, onDelete }) => {
  const handleDelete = useCallback(
    (id: number) => {
      onDelete(id);
    },
    [onDelete]
  );

  return (
    <ul>
      {transactions.map((tx) => (
        <TransactionItem key={tx.id} {...tx} onDelete={handleDelete} />
      ))}
    </ul>
  );
};

export default TransactionList;

Что здесь оптимизировано:

  • TransactionItem обёрнут в React.memo — не будет ререндериться без изменения пропсов.
  • handleDelete мемоизирован с помощью useCallback — не пересоздаётся при каждом рендере списка.

Общие ошибки и рекомендации

  1. Мемоизируйте только "тяжёлые" компоненты. Не нужно оборачивать React.memo всё подряд. Часто проще перерендерить лёгкий компонент, чем проверять, изменились ли пропсы.
  2. Следите за зависимостями в useCallback. Если забыть добавить зависимость, функция может использовать устаревшие значения.
  3. Не усложняйте код ради оптимизации. Если производительность нормальная, лучше оставить код простым.
  4. Сравнение объектов и массивов всегда возвращает false. Даже если массив [] выглядит одинаково, это два разных объекта в памяти.
2
Задача
Модуль 3: React, 19 уровень, 7 лекция
Недоступна
Использование React.memo для предотвращения лишних рендеров
Использование React.memo для предотвращения лишних рендеров
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ