Стратегии кеширования

TanStack Query строит работу вокруг концепции серверного состояния. В отличие от локального состояния интерфейса, серверные данные обладают рядом особенностей:

  • могут устаревать;
  • синхронизируются между несколькими компонентами;
  • обновляются асинхронно;
  • требуют повторной загрузки;
  • могут быть разделены между вкладками и страницами;
  • зависят от времени жизни и актуальности.

Кеширование в TanStack Query предназначено для решения этих задач автоматически. Библиотека хранит результаты запросов в Query Cache, повторно использует данные между компонентами, контролирует устаревание и управляет повторными запросами.


Архитектура кеша

Каждый запрос в TanStack Query идентифицируется через queryKey.

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
});

Ключ запроса становится идентификатором записи в кеше.

Структура кеша условно выглядит следующим образом:

{
    queries: [
        {
            queryKey: ['users'],
            state: {
                data: [...],
                status: 'success',
                fetchStatus: 'idle',
                dataUpdatedAt: 1710000000000
            }
        }
    ]
}

Кеш существует внутри экземпляра QueryClient.

const queryClient = new QueryClient();

Все компоненты приложения используют общий клиент через QueryClientProvider.


Жизненный цикл кеша

После выполнения запроса TanStack Query:

  1. Выполняет queryFn
  2. Сохраняет результат в кеше
  3. Связывает данные с queryKey
  4. Передаёт данные подписанным компонентам
  5. Следит за временем актуальности

Пример:

const query = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
});

Если другой компонент вызывает тот же queryKey, сетевой запрос не выполняется повторно — данные берутся из кеша.


Fresh и Stale данные

Одной из ключевых концепций является разделение данных на:

  • fresh — актуальные;
  • stale — устаревшие.

По умолчанию данные становятся stale сразу после получения.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
});

Это означает:

  • данные отображаются из кеша;
  • TanStack Query может инициировать фоновое обновление.

staleTime

Параметр staleTime определяет, сколько времени данные считаются актуальными.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 1000 * 60
});

В течение одной минуты:

  • запрос не считается устаревшим;
  • повторный mount компонентов не вызывает refetch;
  • данные берутся исключительно из кеша.

Стратегии staleTime

Агрессивное обновление

staleTime: 0

Подходит для:

  • финансовых данных;
  • чатов;
  • уведомлений;
  • realtime-интерфейсов.

Преимущества:

  • минимальный риск устаревших данных.

Недостатки:

  • большое количество сетевых запросов.

Умеренное кеширование

staleTime: 1000 * 30

Хорошо подходит для:

  • списков товаров;
  • блогов;
  • каталогов;
  • CRM-систем.

Позволяет сократить количество запросов без заметной потери актуальности.


Долгоживущий кеш

staleTime: 1000 * 60 * 60

Используется для:

  • справочников;
  • стран;
  • валют;
  • ролей пользователей;
  • редко изменяемых данных.

Бесконечная актуальность

staleTime: Infinity

Данные никогда не считаются stale автоматически.

Обновление возможно только:

  • через invalidation;
  • ручной refetch;
  • обновление кеша.

Пример:

useQuery({
    queryKey: ['countries'],
    queryFn: fetchCountries,
    staleTime: Infinity
});

cacheTime и gcTime

В современных версиях TanStack Query используется gcTime.

Ранее использовался параметр cacheTime.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    gcTime: 1000 * 60 * 5
});

gcTime определяет:

сколько времени неиспользуемый запрос хранится в памяти после удаления всех подписчиков.


Поведение gcTime

Последовательность работы:

  1. Компонент размонтируется
  2. Запрос становится inactive
  3. Запускается таймер gcTime
  4. После истечения времени кеш удаляется

Различие staleTime и gcTime

staleTime

Отвечает за актуальность данных.

gcTime

Отвечает за время хранения в памяти.


Пример различий

useQuery({
    queryKey: ['profile'],
    queryFn: fetchProfile,
    staleTime: 1000 * 60,
    gcTime: 1000 * 60 * 10
});

Что происходит:

  • 1 минута — данные fresh;
  • после 1 минуты — stale;
  • 10 минут после unmount — данные остаются в кеше;
  • затем кеш удаляется.

Background Refetch

Одной из сильнейших стратегий TanStack Query является фоновое обновление.

Когда stale данные уже есть в кеше:

  1. интерфейс мгновенно отображает старые данные;
  2. выполняется refetch;
  3. после получения новых данных UI обновляется.

Это устраняет:

  • пустые состояния;
  • мерцание интерфейса;
  • повторные спиннеры.

refetchOnMount

Управляет повторным запросом при mount компонента.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    refetchOnMount: true
});

Варианты:

refetchOnMount: true
refetchOnMount: false
refetchOnMount: 'always'

Стратегии refetchOnMount

true

Refetch только stale данных.

false

Полное отключение автоматического refetch.

always

Запрос выполняется всегда.


refetchOnWindowFocus

TanStack Query умеет автоматически обновлять данные при возврате во вкладку.

useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications,
    refetchOnWindowFocus: true
});

Когда пользователь возвращается в браузер:

  • stale запросы автоматически обновляются.

Когда полезен refetchOnWindowFocus

Особенно полезно для:

  • административных панелей;
  • dashboards;
  • систем мониторинга;
  • уведомлений;
  • совместной работы.

Когда лучше отключать

refetchOnWindowFocus: false

Подходит для:

  • медленных API;
  • тяжёлых запросов;
  • статических данных;
  • мобильных приложений с дорогим трафиком.

refetchOnReconnect

Запросы могут автоматически обновляться после восстановления интернета.

useQuery({
    queryKey: ['messages'],
    queryFn: fetchMessages,
    refetchOnReconnect: true
});

polling и интервальное обновление

TanStack Query поддерживает периодический refetch.

useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
    refetchInterval: 5000
});

Запрос будет выполняться каждые 5 секунд.


Интеллектуальный polling

Polling автоматически останавливается:

  • при unmount;
  • при offline;
  • при скрытой вкладке.

Поведение можно изменить.


refetchIntervalInBackground

useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
    refetchInterval: 5000,
    refetchIntervalInBackground: true
});

Позволяет обновлять данные даже в неактивной вкладке.


Стратегия Keep Previous Data

При пагинации TanStack Query способен сохранять старые данные во время загрузки новых.

useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    placeholderData: keepPreviousData
});

Проблема без keepPreviousData

Без этой стратегии интерфейс:

  1. очищается;
  2. показывает loading;
  3. загружает новые данные.

Возникает мерцание UI.


Поведение keepPreviousData

Старые данные остаются отображёнными до завершения нового запроса.

Это особенно важно для:

  • таблиц;
  • пагинации;
  • бесконечных списков;
  • поиска.

placeholderData

Позволяет подставлять временные данные.

useQuery({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id),
    placeholderData: {
        title: 'Загрузка...',
        body: ''
    }
});

Placeholder не попадает в кеш как реальные данные.


initialData

initialData работает иначе.

useQuery({
    queryKey: ['settings'],
    queryFn: fetchSettings,
    initialData: defaultSettings
});

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

  • данные сразу считаются полученными;
  • initialData записывается в кеш;
  • может использоваться SSR и hydration.

Prefetching как стратегия кеширования

Предварительная загрузка позволяет заполнить кеш заранее.

await queryClient.prefetchQuery({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id)
});

Когда компонент откроется:

  • данные уже будут в кеше;
  • UI отобразится мгновенно.

Где используется prefetching

Наиболее распространённые сценарии:

  • hover по ссылке;
  • переход между страницами;
  • SSR;
  • роутинг;
  • предварительная загрузка пагинации.

Кеширование пагинации

Каждая страница получает собственный cache entry.

useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page)
});

Страница 1:

['posts', 1]

Страница 2:

['posts', 2]

Infinite Query Cache

Для бесконечных списков используется отдельная стратегия хранения.

useInfiniteQuery({
    queryKey: ['feed'],
    queryFn: fetchFeed,
    getNextPageParam: lastPage => lastPage.nextCursor
});

Кеш содержит массив страниц:

{
    pages: [
        [...],
        [...],
        [...]
    ]
}

Стратегии invalidation

Кеш можно инвалидировать вручную.

queryClient.invalidateQueries({
    queryKey: ['posts']
});

После invalidation:

  • данные помечаются stale;
  • активные запросы обновляются.

Частичная invalidation

queryClient.invalidateQueries({
    queryKey: ['posts']
});

Инвалидирует:

['posts']
['posts', 1]
['posts', 2]
['posts', 'popular']

Точная invalidation

queryClient.invalidateQueries({
    queryKey: ['posts', 1],
    exact: true
});

Затрагивается только один ключ.


Стратегии ручного обновления кеша

Иногда выгоднее обновлять кеш напрямую.

queryClient.setQueryData(
    ['post', id],
    old => ({
        ...old,
        likes: old.likes + 1
    })
);

Преимущества:

  • отсутствие дополнительного запроса;
  • мгновенный UI;
  • уменьшение нагрузки на API.

Оптимистическое кеширование

Optimistic Updates временно изменяют кеш до ответа сервера.

useMutation({
    mutationFn: updatePost,
    onMutate: async updatedPost => {
        await queryClient.cancelQueries({
            queryKey: ['post', updatedPost.id]
        });

        const previousPost = queryClient.getQueryData([
            'post',
            updatedPost.id
        ]);

        queryClient.setQueryData(
            ['post', updatedPost.id],
            old => ({
                ...old,
                ...updatedPost
            })
        );

        return { previousPost };
    }
});

Rollback стратегии

При ошибке кеш можно откатить.

onError: (error, variables, context) => {
    queryClient.setQueryData(
        ['post', variables.id],
        context.previousPost
    );
}

Стратегии разделения query keys

Правильная структура query keys критически важна для кеширования.


Плохая стратегия

['data']

Все данные смешиваются в один cache entry.


Хорошая стратегия

['posts']
['posts', page]
['post', id]
['post-comments', id]

Преимущества:

  • точечная invalidation;
  • независимое обновление;
  • предсказуемый кеш;
  • минимальные refetch.

Нормализация и денормализация кеша

TanStack Query не требует нормализованного кеша как Redux Toolkit Query или Apollo.

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

['posts']
['post', id]

Данные могут дублироваться между запросами.

Преимущества:

  • простота;
  • предсказуемость;
  • меньше логики синхронизации.

Garbage Collection

TanStack Query автоматически очищает неиспользуемые данные.

Механизм GC предотвращает:

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

Persisted Cache

Кеш может сохраняться между перезагрузками страницы.

Для этого используется persistence.

Пример:

import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';

const persister = createSyncStoragePersister({
    storage: window.localStorage
});

persistQueryClient({
    queryClient,
    persister
});

Преимущества persisted cache

После перезагрузки:

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

Offline-first стратегии

TanStack Query поддерживает offline caching.

Данные могут:

  • читаться из кеша без сети;
  • синхронизироваться после reconnect;
  • сохраняться локально.

networkMode

Позволяет управлять поведением сети.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    networkMode: 'offlineFirst'
});

Варианты:

'online'
'always'
'offlineFirst'

Стратегии для разных типов данных

Realtime данные

staleTime: 0
refetchInterval: 3000

Административные панели

staleTime: 10000
refetchOnWindowFocus: true

Справочники

staleTime: Infinity
gcTime: Infinity

Пагинация

placeholderData: keepPreviousData

SSR

initialData
prefetchQuery
dehydrate
hydrate

Глобальные стратегии QueryClient

Настройки можно определить централизованно.

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 1000 * 60,
            gcTime: 1000 * 60 * 10,
            refetchOnWindowFocus: false,
            retry: 2
        }
    }
});

Retry как часть стратегии кеширования

TanStack Query автоматически повторяет запросы.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    retry: 3
});

retryDelay

retryDelay: attempt =>
    Math.min(1000 * 2 ** attempt, 30000)

Используется exponential backoff.


Дедупликация запросов

Если несколько компонентов одновременно вызывают одинаковый запрос:

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
});

TanStack Query:

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

Это одна из важнейших встроенных стратегий оптимизации.


Structural Sharing

TanStack Query оптимизирует обновления через structural sharing.

Если часть объекта не изменилась:

  • ссылки сохраняются;
  • уменьшается количество ререндеров;
  • повышается производительность.

Селективные подписки

Компоненты могут подписываться только на часть данных.

useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    select: data => data.profile
});

Это снижает количество лишних обновлений UI.


Комбинирование стратегий

На практике используется комбинация нескольких подходов:

useQuery({
    queryKey: ['dashboard'],
    queryFn: fetchDashboard,
    staleTime: 30000,
    gcTime: 1000 * 60 * 10,
    refetchOnWindowFocus: true,
    refetchInterval: 60000
});

Подобная конфигурация:

  • минимизирует сетевую нагрузку;
  • сохраняет актуальность;
  • уменьшает loading-состояния;
  • обеспечивает быстрый интерфейс.