Производительность frontend — это меньше лишней работы: лишних перерисовок и лишнего кода, который грузит браузер. Но главная ошибка тут — оптимизировать наугад: обвешать всё useMemo «на всякий случай», усложнив код и ничего не ускорив. Правило одно: сначала измерить, потом оптимизировать, и оптимизировать то, что реально медленно.

Перерисовки и преждевременная оптимизация

React перерисовывает компонент при изменении его состояния или пропсов — и большинство таких перерисовок дёшевы. Перерисовка ≠ перерисовка DOM: React сравнивает результат и трогает реальный DOM только там, где есть разница. Поэтому борьба с каждой перерисовкой обычно не нужна и вредна — она добавляет код и баги ради выигрыша, которого нет.

Признак реальной проблемы — заметная задержка ввода или прокрутки, видимая в профайлере, а не «компонент рендерится N раз». Профилировать — React DevTools Profiler: он показывает, что и почему рендерится и сколько это стоит.

memo, useMemo, useCallback — по делу

Эти инструменты нужны точечно, когда профайлер показал проблему.

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

useMemo — кеширует результат дорогого вычисления между рендерами.

const sorted = useMemo(
  () => products.slice().sort((a, b) => a.price - b.price),
  [products],
);

useCallback — стабилизирует ссылку на функцию, чтобы не ломать React.memo-потомка или массив зависимостей эффекта.

Когда они вредят: на дешёвых вычислениях useMemo стоит дороже самого вычисления; useCallback без memo-потомка — просто шум; обмемоизированный код труднее читать. Эвристика: не добавляй мемоизацию, пока профайлер не показал, что без неё медленно.

Code-splitting

Второй рычаг — не грузить сразу весь код. React.lazy + Suspense подгружают компонент, когда он понадобился; чаще всего делят по маршрутам.

const Dashboard = React.lazy(() => import("./Dashboard"));

<Suspense fallback={<Spinner />}>
  <Dashboard />
</Suspense>

Так первый экран грузит только свой код, а тяжёлые разделы (редактор, графики) подтягиваются по требованию. Это прямо ускоряет первую загрузку — метрику, которую чувствует пользователь.

Размер бандла

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

Принцип тот же, что и с рендером: измеряй (анализатор, Lighthouse), оптимизируй найденное, а не воображаемое.

Где это в UCP

Производительность — инженерное решение по измерению, а не по интуиции: большинство перерисовок дёшевы, мемоизация — точечно по профайлеру, code-splitting и контроль бандла — за быструю первую загрузку. Это то же «не оптимизируй преждевременно, профилируй перед оптимизацией», что в backend. Для продукт-инженера скорость интерфейса — часть качества продукта, которую он держит измеримо; а закрепляется всё это тестами, проверяющими поведение, а не детали оптимизаций.