Основы SSR в React 19.2: что такое Activity, cacheSignal и PPR — подробное объяснение
Узнайте, как новый компонент Activity, функция cacheSignal и технология частичной предварительной отрисовки в React 19.2 дают разработчикам прямой контроль над производительностью серверной отрисовки.
Работа над производительностью в React обычно делится на две категории: ускорение и оптимизация первоначальной обработки на сервере, а также предотвращение ненужной обработки на стороне клиента после его загрузки. В этой статье эти два аспекта рассматриваются с противоположных точек зрения, но они основаны на одной и той же философии — скорость достигается путем четкого указания React, какие операции важны, что можно отложить и что вообще не следует выполнять, а не путем увеличения объема кэширования или использования дополнительного оборудования. Первая часть посвящена серверной обработке и новым примитивам, появившимся в React 19.2; вторая рассматривает повседневные техники работы на стороне клиента, которые обеспечивают отзывчивость загруженного приложения.
Переосмысление производительности серверной обработки в React 19.2
Большинство рекомендаций по «производительности SSR» сводятся к добавлению слоев кэширования и надежде на лучшее решение. React 19.2, выпущенный в октябре 2025 года, предоставляет специальные примитивы для прямого управления работой сервера. Рассматривать эту версию как небольшое обновление означает упустить реальные улучшения скорости — именно описанные ниже моменты действительно влияют на производительность.
Сохраняйте компоненты активными вместо их уничтожения
Частой проблемой в приложениях SSR является то, что смена вкладок, открытие модалок и переходы между маршрутами полностью уничтожают компоненты, стирая их состояние и заставляя снова загружать данные. Новый компонент Activity создан именно для решения этой проблемы.
// old way — full unmount, state gone, effects re-run
{activeTab === 'analytics' && <AnalyticsPanel />}
// React 19.2 — stays alive, just deprioritized
<Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}>
<AnalyticsPanel />
</Activity>
Благодаря тому, что React сохраняет отображение скрытого контента вместо его уничтожения, он может заранее загрузить содержимое скрытого блока Activity ещё до того, как пользователь нажмет на него. Такой подход снижает воспринимаемую задержку навигации на панелях управления с интенсивной обработкой на сервере, поскольку не происходит повторной загрузки и смены макета при становлении контента видимым.
Позвольте cacheSignal автоматически удалять незавершённые операции
До версии 19.2, если обработка на сервере прерывалась на полпути — например, пользователь ушёл или запрос истек временем — любые запущенные запросы и сохранённые данные не могли узнать, что они больше не нужны. cacheSignal решает эту проблему, предоставляя компонентам React Server Components реальный сигнал жизненного цикла для выполнения операций по очистке.
async function getUserOrders(userId, { signal }) {
const res = await fetch(`/api/orders/${userId}`, { signal });
return res.json();
}
Как только истекает срок жизни кэша, соответствующий сигнал запускает процедуру прерывания, что предотвращает дальнейшее использование ресурсов CPU сервером для обработки оставшихся запросов в моменты пиковой нагрузки.
Создание статической оболочки и стриминг остального контента
Частичная предварительная обработка (PPR) — это ключевая функция SSR в этом выпуске. Суть заключается в том, чтобы один раз сгенерировать статическую оболочку страницы — навигацию, макет, футер — и передать её напрямую из CDN, а динамические элементы загружать постепенно в рамках границ Suspense.
<Suspense fallback={<ProductSkeleton />}>
<PartialPreRender>
<PersonalizedRecommendations userId={user.id} />
</PartialPreRender>
</Suspense>
Объединение нескольких операций Suspense в одну
Ранее, когда несколько границ состояния Suspense решались примерно в один и тот же момент, интерфейс мог столкнуться с эффектом «попкорна», при котором фрагменты контента появлялись один за другим, а не сразу. React 19.2 объединяет эти операции в группы, чтобы поведение клиента и сервера оставалось последовательным, а также добавляет поддержку Web Streams в Node.js для команд, нуждающихся в более тонком контроле над потоковой передачей данных.
Эти API повышают стандарт, а не понижают его
Ничто из этого не заменяет основы. Вам по-прежнему необходимо устранять паттерны загрузки данных типа N+1 и разбивать монолитные пакеты, прежде чем эти функции смогут вам хоть как-то помочь. React 19.2 не снижает минимальное количество необходимых работ по оптимизации — он лишь повышает верхний предел скорости, достижимой хорошо оптимизированным приложением. Также стоит прочитать официальные записки об обновлении React 19.2, а также информацию о стандартных настройках Turbopack, введенных в Next.js 16, которые хорошо сочетаются с PPR.
К 2026 году производительность SSR заключается не в более агрессивном кэшировании — она связана с тем, чтобы указать React, что может ждать, что можно передавать потоком и что может сработать некорректно, но при этом сохранить стабильность. React 19.2 наконец предоставляет инструменты для выражения этих идей.
Помимо этих механизмов, специфичных для SSR, многие факторы, делающие приложение React быстрым, связаны с повседневными привычками, которые сохраняются независимо от стратегии отрисовки или версии React. Если в предыдущем разделе рассматривались технологии потоковой отрисовки, предварительной обработки и механизм Suspense на стороне сервера, то далее речь пойдет о клиентских подходах, которые обеспечивают отзывчивость любого приложения React на практике.
Практические методы для повышения производительности React в повседневной работе
React по умолчанию отрисовывает контент эффективно. Однако по мере роста приложения начинают накапливаться ненужные перерисовки, слишком большие списки, избыточный JavaScript и множество сетевых запросов. Для решения проблемы редко требуются сложные приемы — достаточно соблюдения нескольких четко определенных привычек, чтобы значительно улучшить производительность.
Отрисовывайте только то, что действительно нуждается в обновлении
Каждая перерисовка заставляет React снова выполнять тело функции компонента. В себе это не является проблемой — настоящая трата ресурсов происходит при повторном выполнении дорогостоящих операций, когда на самом деле ничего существенного не изменилось. Избегайте размещения нерелевантных состояний внутри компонента, отвечающего за отображение большой части интерфейса, поскольку обновление такого состояния принудительно заставляет перерисоваться всю его вложенную структуру. Вместо этого разделяйте компоненты, чтобы обновление затрагивало только ту часть интерфейса, на которую оно действительно предназначено. Цель не в полном отсутствии перерисовок — а в устранении тех, которые являются избыточными.
Размещайте состояние там, где оно действительно нужно
Не старайтесь перемещать каждый элемент состояния на вершину дерева компонентов. Если только один компонент использует определенное значение, именно там оно и должно находиться.
function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
Хранение состояния локально в таком формате ограничивает распространение обновлений, что снижает количество ненужных перерисовок в других частях структуры. Как правило, следует размещать состояние рядом с компонентом, который им пользуется, а не выше в иерархии.
Вычисляйте значения вместо их хранения
Не всё должно находиться в useState. Если вы уже отслеживаете firstName и lastName, нет причин хранить fullName отдельно. Ненужная версия выглядит так:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Проще всего вычислять значение прямо во время рендеринга:
const fullName = `${firstName} ${lastName}`;
Это позволяет избавиться как от дополнительной переменной состояния, так и от ненужного Effect. В целом, если значение можно вычислить в процессе рендеринга, скорее всего, оно не требует хранения в состоянии.
Обработка больших списков без перегрузки DOM
Обработка тысяч узлов одновременно быстро становится дорогостоящей. Представьте экран чата с 10 000 сообщениями — нет необходимости, чтобы все они существовали в DOM одновременно. Для больших коллекций используйте один из следующих методов:
- Виртуализация
- Пагинация
- Бесконечное прокручивание
При виртуализации в любой момент времени отображаются только строки, которые видны в настоящее время, плюс небольшой буфер; библиотеки вроде react-window реализуют эту схему автоматически. Однако не применяйте виртуализацию бездумно — список из 50 элементов практически наверняка её не требует.
Откладывайте загрузку кода до момента его необходимости
Пользователи не должны загружать JavaScript для функций, которые они ещё не открыли. API React lazy и Suspense позволяют вынести такой код из начального пакета:
const Settings = lazy(() => import("./Settings"));
Благодаря этому компонент настроек загружается только тогда, когда он действительно отрисовывается, а не вместе с первоначальной загрузкой страницы. Это полезно для тяжелых или редко используемых функций — диаграмм, редакторов, карт, экранов настроек, крупных панелей управления — и в целом способствует более быстрой первоначальной загрузке.
Поддержание отзывчивости интерактивного пользовательского интерфейса при высокой нагрузке
Не каждое обновление должно происходить синхронно с действиями пользователя. Поле поиска должно реагировать на нажатия клавиш мгновенно, даже если фильтрация большого набора данных выполняется на фоне с меньшим приоритетом. Хуки React useTransition и useDeferredValue созданы именно для такого компромисса. Ограничение частоты ввода данных также дает похожий эффект:
const debouncedSearch = useDebounce(search, 500);
Вместо отправки запроса при каждом нажатии клавиши ждите, пока пользователь сделает паузу. Такой подход полезен для полей поиска, фильтров, длинных списков и панелей управления с большим количеством динамических элементов.
Сокращение избыточных сетевых запросов
Скорость отрисовки — это лишь часть картины: слишком много активных запросов могут заставить приложение казаться медленным, даже если сама отрисовка происходит быстро. В зависимости от ситуации рассмотрите следующее:
- Хранение ответов в кэше
- Устранение дублирования запросов
- Пагинация результатов
- Задержка обработки ввода в поле поиска
- Отмена запросов, которые больше не актуальны
Если пользователь быстро печатает
react
react performance
react performance optimization
вам, скорее всего, не нужно, чтобы три отдельных запроса работали одновременно. Основной принцип остается простым: избегайте задач для сети, которые вам на самом деле не нужны.
Используйте мемоизацию выборочно
React предоставляет три распространённых инструмента мемоизации:
- useMemo хранит результат вычисления в кэше.
- useCallback сохраняет ссылку на функцию между перерисовками.
- React.memo позволяет пропустить перерисовку компонента, если его параметры не изменились.
Типичный пример:
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.includes(search)
);
}, [users, search]);
Однако мемоизация не бесплатна — она сопряжена со своими затратами и увеличивает сложность окружающего кода. Используйте её тогда, когда вычисления действительно затратны, когда компонент постоянно перерисовывается без причины, или когда стабильная ссылка действительно важна дальше по иерархии. Не тратите усилия на оптимизацию кода, который не вызывает ощутимых проблем.
Пусть компилятор возьмёт на себя часть работы
Новый компонент инструментария React — React Compiler — может автоматически применять многие из этих оптимизаций вместо вас, создавая кэш для значений, функций и компонентов без необходимости вручную писать код в каждом случае. Это означает, что вам больше не нужно обязательно использовать:
useMemo(...)
useCallback(...)
React.memo(...)
Тем не менее React Compiler не заменяет необходимость сначала выяснить, действительно ли существует проблема с производительностью. Правильный порядок действий по-прежнему заключается в подтверждении наличия реальной проблемы, затем применении оптимизаций, которые может выполнить компилятор, и только при наличии конкретных оснований переходе к ручной мемоизации.
Предоставьте React стабильные идентификаторы
Ключи указывают React, какой элемент в списке является тем или иным при каждом перерисовании. Желательно брать ключ из стабильного, уникального свойства данных, а не из его позиции:
items.map(item => (
<Item key={item.id} />
));
Избегайте использования индекса массива в качестве ключа, поскольку перестановка, вставка или удаление элементов меняют все индексы ниже точки изменения:
items.map((item, index) => (
<Item key={index} />
));
Стабильный ключ позволяет React правильно определять, какие элементы были добавлены, удалены или обновлены, вместо того чтобы делать предположения на основе их положения. Тот же принцип применим к объектам и функциям, которые передаются в качестве пропсов: создание совершенно нового объекта или функции-обратного вызова при каждом отрисовывании сводит на нет цель использования мемизированного дочернего компонента, поскольку его пропсы будут отличаться каждый раз, даже если существенных изменений не произошло.
Определите реальную проблему перед действием
Как только вы освоите эти методы, не полагайтесь на интуицию при принятии решения о том, что нужно исправить. Откройте профилятор React DevTools, чтобы увидеть, какие именно компоненты отрисовываются и сколько времени уходит на каждый из них. При проблемах, выходящих за рамки самого React, панель производительности браузера может выявить длительные операции, медленную работу скриптов, затратную обработку лейаута или узкие места в процессе отрисовки. Вместо того чтобы предполагать, что «этот компонент работает медленно», используйте эти инструменты, чтобы выяснить причину медленной работы.
Подтверждение того, что исправление действительно помогло
После применения оптимизации снова измерьте показатели, вместо того чтобы просто предполагать, что она сработала. Проверьте, действительно ли время отрисовки сократилось, стал ли бандл меньше по размеру и стали ли интеракции быстрее отвечать на действия. Если ничего из этого не улучшилось, возможно, изменения вообще были ненужны.
Чек-лист перед выпуском
Перед публикацией приложения на React проверьте, не отображается ли интерфейс, который вам не нужен, находится ли состояние в правильном месте, хранятся ли значения, которые можно было бы рассчитать заново, эффективно ли обрабатываются большие списки, загружается ли тяжелый код только при необходимости, остаются ли поисковые функции и интеракции отзывчивыми, совершаются ли ненужные запросы к API, решает ли мемоизация реальную проблему, может ли React Compiler взять на себя оптимизацию, стабильны ли ключи, измерен ли настоящий узкий место, и проверена ли улучшение после этого.
В конечном итоге лучшая оптимизация — это не та, которая добавляет больше всего кода, а та, которая заставляет React выполнять меньше ненужной работы.
Связанные материалы
- Понимание React Lanes: как битовые маски кодируют приоритет обновлений — Узнайте, как React использует битовые маски для кодирования нескольких приоритетов обновлений в одно целое число, и почему для планирования задач применяются битовые операции вместо простых логических флагов.
- Компилятор TypeScript на Go и нативная экспекуция: руководство по миграции — Узнайте, как компилятор TypeScript на основе Go и нативная экспекуция в Node.js повлияют на кодовые базы React и Next.js, и что необходимо исправить в вашем tsconfig прямо сейчас.