Предзагрузка данных на сервере

Предзагрузка данных на сервере (server prefetching) в RTK Query используется для формирования заранее заполненного кэша Redux-состояния до того, как приложение будет отрисовано на клиенте. Это особенно важно в сценариях SSR (Server-Side Rendering), где необходимо отдать пользователю уже готовую страницу с данными, а не пустой интерфейс с последующей загрузкой.

RTK Query предоставляет набор механизмов, которые позволяют извлекать данные на сервере и синхронизировать их с Redux store таким образом, чтобы клиентская часть приложения сразу работала с уже подготовленным кэшем.


Основная концепция предзагрузки

RTK Query строит работу вокруг кэша запросов. Каждый endpoint описывает запрос, который может быть выполнен как на клиенте, так и на сервере. При серверной предзагрузке происходит следующее:

  • создаётся Redux store на сервере;
  • выполняются нужные запросы через dispatch;
  • результаты сохраняются в RTK Query cache;
  • сформированное состояние сериализуется и передаётся на клиент.

Ключевая особенность заключается в том, что RTK Query не делает различий между серверным и клиентским выполнением запроса — различие заключается только в окружении выполнения dispatch.


Базовый механизм prefetch через initiate

Каждый endpoint в RTK Query генерирует thunk initiate, который может быть вызван напрямую через store.dispatch.

store.dispatch(api.endpoints.getUser.initiate(userId));

При серверной предзагрузке этот вызов инициирует запрос, а результат автоматически попадает в кэш.

После завершения всех необходимых запросов обычно требуется дождаться их завершения через await.

await store.dispatch(api.endpoints.getUser.initiate(userId));

Если запрос уже существует в кэше, RTK Query может избежать повторного сетевого вызова в зависимости от настроек forceRefetch и времени keepUnusedDataFor.


Сбор состояния после предзагрузки

После выполнения всех серверных запросов необходимо получить сериализованное состояние Redux store:

const preloadedState = store.getState();

Это состояние содержит:

  • данные RTK Query cache;
  • metadata о статусе запросов;
  • ошибки, если они возникли на сервере.

Далее оно передаётся в клиентскую часть приложения как initialState.


Гидратация кэша на клиенте

На клиенте создаётся store с использованием уже готового состояния:

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  preloadedState,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
});

RTK Query автоматически распознаёт, что кэш уже существует, и использует его без повторных запросов.

При этом система сравнивает:

  • queryCacheKey;
  • параметры запроса;
  • актуальность данных.

Если данные считаются свежими, повторный fetch не выполняется.


Prefetch через api.util.prefetch

Помимо прямого initiate, RTK Query предоставляет утилиту prefetch, которая предназначена для сценариев, где необходимо заранее подготовить данные без немедленного использования результата.

store.dispatch(api.util.prefetch('getUser', userId, { force: true }));

Поведение:

  • запускает запрос;
  • не требует подписки компонента;
  • сохраняет результат в кэш;
  • полезна для маршрутизаторов и SSR-слоёв.

В отличие от initiate, prefetch не возвращает результат запроса как основную цель, а ориентирована именно на подготовку данных.


Предзагрузка с учётом маршрутов

В серверных приложениях часто используется привязка предзагрузки к маршрутам. Каждый маршрут может определять набор данных, необходимых для рендера.

Пример логики:

  • маршрут /user/:id требует getUser;
  • маршрут /posts/:id требует getPost и комментарии;
  • сервер анализирует маршрут и инициирует соответствующие запросы.

RTK Query в этом случае выступает как слой кэширования и выполнения запросов, а логика маршрутизации остаётся внешней.


Параллельная предзагрузка нескольких запросов

RTK Query поддерживает одновременное выполнение нескольких thunk-операций:

await Promise.all([
  store.dispatch(api.endpoints.getUser.initiate(userId)),
  store.dispatch(api.endpoints.getPosts.initiate()),
]);

Особенности:

  • запросы выполняются параллельно;
  • каждый результат помещается в свой slice кэша;
  • store остаётся консистентным;
  • можно ожидать завершения всех операций через Promise.all.

Контроль повторных запросов при SSR

При повторной инициализации на сервере важно учитывать дублирование запросов. RTK Query предотвращает лишние вызовы через:

  • дедупликацию одинаковых запросов;
  • временное окно keepUnusedDataFor;
  • проверку активных подписок.

Однако в SSR-контексте подписок может не быть, поэтому важную роль играет явное управление:

  • использование forceRefetch;
  • очистка store между запросами;
  • создание отдельного store на каждый SSR-запрос.

Инвалидация кэша при серверной предзагрузке

RTK Query поддерживает tag-based invalidation, что сохраняется и в SSR-режиме.

Если endpoint использует providesTags:

getUser: builder.query({
  query: (id) => `user/${id}`,
  providesTags: (result, error, id) => [{ type: 'User', id }],
});

то серверный кэш может быть структурирован таким образом, что последующие предзагрузки автоматически учитывают изменения данных через:

dispatch(api.util.invalidateTags([{ type: 'User', id }]));

Это позволяет управлять актуальностью данных ещё до передачи состояния клиенту.


Использование selectFromResult при серверной подготовке

Хотя selectFromResult чаще применяется на клиенте для оптимизации ререндеров, его логика косвенно влияет на SSR:

  • сервер формирует полный кэш;
  • клиент выбирает только необходимые фрагменты данных;
  • структура селекторов определяет форму потребления данных.

Таким образом, SSR создаёт «полную базу», а селекторы формируют «проекции» этой базы.


Ошибки и обработка состояния предзагрузки

При серверной предзагрузке важно учитывать, что ошибки запросов также попадают в кэш:

  • isError сохраняется;
  • error сериализуется;
  • статус запроса фиксируется как rejected.

Это позволяет клиенту корректно отобразить состояние без повторного запроса.

Пример состояния:

  • status: 'rejected'
  • error: { message, code }

RTK Query не скрывает ошибки при SSR, что позволяет реализовывать полноценную серверную обработку.


Сериализация состояния и ограничения

Redux state, содержащий RTK Query cache, должен быть сериализуемым. Основные ограничения:

  • нельзя хранить функции;
  • нельзя хранить классы или циклические структуры;
  • ошибки должны быть сериализуемыми объектами.

RTK Query по умолчанию соблюдает эти ограничения, но при кастомных трансформациях данных важно учитывать совместимость с JSON-сериализацией.


Оптимизация предзагрузки

При больших приложениях предзагрузка может становиться дорогостоящей операцией. Основные стратегии оптимизации:

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

RTK Query эффективно работает в связке с частичной SSR-стратегией, где не все данные загружаются заранее, а только критические для первого рендера.


Предзагрузка через кастомный SSR слой

В сложных архитектурах поверх RTK Query строится отдельный слой:

  • анализ маршрута;
  • определение зависимостей данных;
  • сбор списка endpoints;
  • выполнение dispatch-операций;
  • передача preloadedState.

RTK Query в этом случае остаётся источником истины для данных, а SSR слой — оркестратором запросов.


Поведение повторной гидратации

При повторной загрузке страницы клиентская часть:

  • считывает preloadedState;
  • инициализирует RTK Query cache;
  • синхронизирует internal state;
  • предотвращает повторные запросы, если данные актуальны.

Если состояние устарело, RTK Query автоматически инициирует refetch в соответствии с политиками refetchOnMountOrArgChange.


Значение предзагрузки для производительности

Серверная предзагрузка снижает:

  • время до первого контента (FCP);
  • количество клиентских запросов;
  • нагрузку на сеть после загрузки страницы.

RTK Query делает этот процесс предсказуемым за счёт единого механизма кэширования, который одинаково работает на сервере и клиенте, устраняя необходимость дублирования логики получения данных.