Измерение всегда должно предшествовать оптимизации. Слишком многие команды оптимизируют вслепую: навешивают useMemo и useCallback на каждое значение, которое плохо лежит, ни разу не сняв профиль, чтобы понять, есть ли разница. К сожалению, оптимизация «по приметам» почти всегда делает код медленнее, а не быстрее, потому что мемоизация сама по себе стоит денег: она сравнивает зависимости на каждом рендере и занимает память.
Это руководство охватывает принципы и инструменты оптимизации React в 2026 году. Мы разберём, что делает React быстрым по умолчанию, где на самом деле возникают узкие места и как измерять и оптимизировать, не усложняя код. Основное внимание — серверным компонентам React, переоценке useMemo и useCallback и растущей роли оптимизации на этапе сборки.
Измерение: единственное, что считается
React DevTools по-прежнему первая остановка. Вкладка профайлера показывает время коммита для каждого рендера и подсвечивает, какие именно компоненты обходятся дороже всего. Полезнее всего пламенная диаграмма: длительность коммита закодирована цветом, поэтому выбросы видны сразу. Если выброса не видно, оптимизация только ухудшит код.
// Measuring with the Profiler API
import { Profiler } from "react";
function onRenderCallback(
id: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration.toFixed(2)}ms`);
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}Порог в 16 миллисекунд соответствует 60 кадрам в секунду: любой коммит длиннее вызывает заметные рывки на экране. API профайлера нужен не только в разработке — его безопасно оставить в продакшене с условным логированием, чтобы отслеживать реальную производительность. Самая дешёвая оптимизация, которую вы можете сделать, — добавить метрики: так вы перестаёте гадать и начинаете знать.
- Измеряйте в продакшене. В разработке всё быстрее, а железо ноутбука прячет проблемы, которые проявятся на мобильных устройствах.
- Занимайтесь крупным. Оптимизировать компонент на 200 мс, который рендерится один раз, бессмысленно. Оптимизировать компонент на 20 мс, который рендерится 200 раз, — на вес золота.
- Следите за трендами, а не за отдельными числами. Один замер — это шум; усреднение по нескольким просмотрам страницы — сигнал.
Серверные компоненты React — самый большой выигрыш
Серверные компоненты React (RSC) меняют правила игры, перенося на сервер всё, чему не нужна интерактивность. Серверный компонент выполняется на сервере ровно один раз, выдаёт статический HTML и не отправляет клиенту никакого JavaScript. Это не оптимизация — это устранение целой категории работы.
// Server Component - no client JS, no hydration
// In Next.js App Router, files default to server components
async function ProductList() {
const products = await db.products.findAll(); // direct DB access
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Only this interactive part ships to the client
"use client";
function AddToCart({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => {
addToCart(productId);
setAdded(true);
}}>
{added ? "In cart" : "Add to cart"}
</button>
);
}В примере выше ProductList не отправляет в браузер ни байта JavaScript. Он рендерится на сервере, а клиент получает только готовый HTML. Это не новый трюк оптимизации, а принципиально другая архитектура, где серверная логика не стоит клиенту ничего. Доступ к базе идёт напрямую, без маршрута API, что убирает один сетевой переход. Бандл становится меньше, потому что серверный код в него не попадает. Первая загрузка быстрее, потому что меньше JavaScript нужно скачать, разобрать и выполнить.
Правило простое: по умолчанию — сервер. Только если компоненту нужна интерактивность — useState, useEffect, onClick, браузерные API — помечайте его директивой 'use client'. Всё остальное остаётся серверным. Одно это сокращает клиентский бандл в большинстве приложений на 40–60%.
useMemo и useCallback — сдержанно и прицельно
useMemo и useCallback — самые часто злоупотребляемые API в React. Рекомендация по умолчанию на 2026 год: не используйте их, пока профайлер не скажет, что это необходимо. Причина в том, что мемоизация не бесплатна. Каждый вызов useMemo требует сравнения зависимостей на каждом рендере. Если это сравнение дешевле пересчёта значения, вы экономите время. Если нет — тратите.
// Before - premature memoization
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// This useMemo is either:
// 1. Wasted - if items is a new reference every render, the sort runs anyway
// 2. Correct - if items reference is stable AND sorting 10k+ items
// Profile first, then decide
// After - only memoize when the profiler shows a problem
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
// useCallback - same principle
// Before
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
// After - if the child it passes to doesn't use memo, useCallback is meaningless
const handleClick = () => setCount((c) => c + 1);
// Correct usage - child uses React.memo
const HeavyList = React.memo(function HeavyList({ items }: { items: Item[] }) {
return items.map((item) => <HeavyRow key={item.id} item={item} />);
});
// Now useCallback matters - it keeps the reference stable
const handleClick = useCallback((id: string) => {
selectItem(id);
}, []);
return <HeavyList items={items} onSelect={handleClick} />;Правило большого пальца: если профайлер не показывает дорогого рендера, useMemo ничего не даёт. Если рендер действительно дорог, проверьте, нужен ли он вообще — часто правильный ответ в том, чтобы обернуть компонент в React.memo или перенести его на сервер. useCallback подчиняется тем же правилам: он осмыслен, только если вы передаёте функцию дочернему компоненту, использующему React.memo. Иначе стабильная ссылка ничего не значит, потому что ребёнок всё равно перерисуется.
Исключения есть. Если useEffect зависит от функции или объекта, стабилизация ссылки предотвращает бесконечные повторные запуски. Если вы вычисляете значение по десяткам тысяч записей, мемоизация даёт ощутимую разницу. Но это именно исключения, а не правила. По умолчанию держите код простым и измеряйте.
useTransition — отзывчивость без блокировки
useTransition — самый важный API производительности, которым большинство разработчиков не пользуется. Он позволяет пометить обновления состояния как переходы — обновления, которые можно прервать работой более высокого приоритета. React отдаёт предпочтение отзывчивым обновлениям (ввод, клики) перед переходами. Результат: поле ввода остаётся отзывчивым, даже если результаты поиска появляются не сразу.
function SearchPage() {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const results = useSearch(query);
return (
<div>
<input
value={query}
onChange={(e) => {
// Urgent: update the input value immediately
// Transition: search results can be deferred
startTransition(() => {
setQuery(e.target.value);
});
}}
/>
{isPending ? (
<Spinner />
) : (
<SearchResults results={results} />
)}
</div>
);
}Без useTransition каждое нажатие клавиши, запускающее перерисовку результатов, блокирует поток до конца рендера. Пользователю кажется, что клавиатура подвисает. С useTransition вы сообщаете React, что обновление значения поля важно и должно произойти немедленно, а рендер результатов может подождать. React при необходимости прервёт работу перехода, чтобы пропустить более важные обновления.
useDeferredValue — родственный API для случаев, когда вы не управляете значением сверху (оно приходит из пропа или контекста), но хотите отложить дорогой рендер, зависящий от него. Он принимает значение и возвращает отложенную копию, которая уступает срочным обновлениям.
Разделение кода и анализ бандла
Даже с серверными компонентами клиентского JavaScript остаётся немало. Разделение кода режет бандл на меньшие части, которые загружаются по необходимости. Механизм в React 2026 года — react.lazy в связке с Suspense. Структура серверных компонентов делает это естественнее: клиентские компоненты можно импортировать динамически там, где они используются.
import { Suspense, lazy } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<div>
<Header />
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart /> {/* loaded only when rendered */}
</Suspense>
</div>
);
}Инспектируйте бандл анализатором. Ищите то, что не должно быть большим: целая библиотека, подтянутая ради одной вспомогательной функции, дубли одной и той же библиотеки, случайно импортированные серверные модули. Каждый удалённый килобайт лишнего JavaScript — выигрыш для каждого пользователя при каждом открытии страницы.
Современные сборщики (Turbopack, Vite) разделяют код по маршрутам автоматически, но ручное разделение внутри страницы всё ещё ценно. Крупные диаграммы, редакторы форматированного текста, видеоплееры — всё большое или редко используемое стоит грузить лениво. Граница Suspense даёт естественное состояние загрузки, чтобы пользователь не смотрел на пустой экран.
- Регулярно смотрите на бандл через анализатор. Вы удивитесь тому, что туда попадает.
- Грузите лениво любой компонент тяжелее 20 КБ в сжатом виде, если он не нужен на каждой странице.
- Серверные компоненты меняют динамику размера бандла: динамический импорт в серверном компоненте подтянет клиентский компонент только тогда, когда тот действительно используется.
Виртуальная прокрутка и виртуализация данных
Когда пользователь смотрит на список из тысячи элементов, React не обязан рендерить их все заранее. Достаточно отрисовать те, что попадают в область просмотра, и переиспользовать узлы DOM при прокрутке. Библиотеки вроде react-window и @tanstack/react-virtual дают это малой кровью. Разница в производительности не постепенная — это разница между плавным и непригодным интерфейсом.
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ItemRenderer item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}Хитрость в том, чтобы заменить абсолютное позиционирование трансформацией. Сдвиг контейнера через translateY — операция композиции, которая не вызывает пересчёт раскладки, поэтому прокрутка остаётся плавной на стороне GPU. Библиотека виртуализации точно вычисляет, какие элементы должны быть видны при текущем положении прокрутки, и никогда не рендерит больше, чем помещается на экране. При ста тысячах элементов список по-прежнему держит в DOM всего 10–20 узлов.
Оптимизация изображений и медиа
Изображения вносят наибольший вклад в вес страницы на большинстве сайтов. Каждая неоптимизированная картинка — потеря производительности. Оптимизация изображений в React в 2026 году сводится к использованию next/image из Next.js или эквивалентного компонента вашего фреймворка. Такие компоненты сами делают адаптивные размеры, конвертацию в WebP/AVIF, ленивую загрузку и предотвращение сдвига раскладки.
Для проектов не на Next.js убедитесь, что: (1) у всех тегов <img> заданы width и height, чтобы не было сдвигов раскладки; (2) для изображений ниже сгиба указан loading='lazy'; (3) сгенерированы несколько размеров и используется srcset; (4) в продакшене выполняется конвертация в современные форматы. Эти четыре изменения обычно сокращают время загрузки изображений на 60–80%.
Итог
Производительность React в 2026 году — это больше про архитектуру, чем про приёмы оптимизации. Серверные компоненты меняют значение по умолчанию: основная работа происходит на сервере, где ей и место. Клиентский рендер ограничивается тем, что действительно должно быть интерактивным. Внутри такой архитектуры useMemo и useCallback становятся специальными инструментами, а не общей практикой. Любое решение об оптимизации должно начинаться с показаний профайлера. Сначала измерьте, затем выдвиньте гипотезу, оптимизируйте и проверьте эффект.