Производительность в TanStack Query строится вокруг нескольких ключевых механизмов:
Библиотека решает сразу две проблемы:
В крупных приложениях именно эти два фактора чаще всего становятся причиной деградации интерфейса.
В основе TanStack Query находится QueryCache.
Каждый запрос хранится по уникальному queryKey.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
После выполнения запроса данные попадают в кеш:
{
queryKey: ['users'],
state: {
data,
status,
error,
fetchStatus
}
}
Если другой компонент использует такой же queryKey,
повторный запрос не выполняется.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Оба компонента получают одни и те же данные из кеша.
Это:
TanStack Query автоматически объединяет одинаковые запросы.
Если одновременно вызываются:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
в нескольких местах приложения, библиотека выполняет только один HTTP-запрос.
Остальные подписчики получают тот же Promise.
Без дедупликации возможна ситуация:
Компонент A -> GET /posts
Компонент B -> GET /posts
Компонент C -> GET /posts
TanStack Query преобразует это в:
GET /posts
с общим результатом для всех компонентов.
Это особенно важно:
staleTime определяет, сколько времени данные считаются
свежими.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 1000 * 60 * 5
})
В течение 5 минут:
Маленький staleTime:
staleTime: 0
означает:
Большой staleTime:
staleTime: Infinity
минимизирует запросы, но повышает риск устаревших данных.
staleTime: 5000
Подходит для:
staleTime: 1000 * 60
Подходит для:
staleTime: Infinity
Подходит для:
В TanStack Query v5 параметр cacheTime был переименован
в gcTime.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
gcTime: 1000 * 60 * 10
})
После того как последний подписчик исчезает:
gcTime.gcTime: 0
Плюсы:
Минусы:
gcTime: Infinity
Плюсы:
Минусы:
TanStack Query использует structural sharing.
Если часть объекта не изменилась, ссылка сохраняется.
Пример:
{
users: [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'John' }
]
}
После обновления:
{
users: [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'Bob' }
]
}
Объект пользователя с id: 1 сохранит старую ссылку.
Это уменьшает:
По умолчанию компонент ререндерится при изменении любых полей query state.
Можно ограничить отслеживание:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
notifyOnChangeProps: ['data']
})
Теперь компонент реагирует только на изменения data.
Это снижает количество ререндеров при изменении:
isFetching;fetchStatus;errorUpdatedAt;select позволяет извлекать только нужную часть
данных.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.items
})
Компонент будет зависеть только от items.
Без select:
data.meta
data.pagination
data.permissions
тоже участвуют в сравнении.
Опасная конструкция:
select: (data) => {
return data.items.map(item => ({
...item,
fullName: `${item.first} ${item.last}`
}))
}
select вызывается при каждом обновлении query.
Если данных много:
Лучше выносить тяжелую обработку:
const transformUsers = memoize((users) => {
return users.map(user => ({
...user,
fullName: `${user.first} ${user.last}`
}))
})
select: (data) => transformUsers(data.items)
placeholderData позволяет мгновенно показать данные до
завершения запроса.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
placeholderData: []
})
Преимущества:
initialData отличается от
placeholderData.
useQuery({
queryKey: ['settings'],
queryFn: fetchSettings,
initialData: cachedSettings
})
initialData попадает в настоящий кеш.
placeholderData — временное значение.
При пагинации часто возникает мерцание.
Без оптимизации:
Старая страница -> loading -> новая страница
С keepPreviousData:
useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: keepPreviousData
})
старые данные сохраняются до прихода новых.
Это:
TanStack Query умеет обновлять данные в фоне.
refetchOnWindowFocus: true
При возврате во вкладку:
Это быстрее, чем:
loading -> empty state -> data
useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
refetchInterval: 5000
})
Важно избегать слишком маленьких интервалов.
Плохой вариант:
refetchInterval: 500
Проблемы:
useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
refetchInterval: 5000,
refetchIntervalInBackground: false
})
Когда вкладка скрыта:
Предзагрузка данных уменьшает задержки интерфейса.
queryClient.prefetchQuery({
queryKey: ['post', id],
queryFn: () => fetchPost(id)
})
Когда пользователь открывает страницу:
Популярная техника:
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id)
})
}
Предзагрузка начинается при наведении мыши.
useInfiniteQuery требует осторожности.
Каждая страница хранится в кеше:
{
pages: [...],
pageParams: [...]
}
При бесконечном скролле память может расти бесконтрольно.
Иногда необходимо вручную обрезать страницы:
queryClient.setQueryData(
['feed'],
(data) => ({
...data,
pages: data.pages.slice(-5),
pageParams: data.pageParams.slice(-5)
})
)
Это уменьшает:
Optimistic updates уменьшают perceived latency.
onMutate: async (newTodo) => {
await queryClient.cancelQueries(['todos'])
const previousTodos =
queryClient.getQueryData(['todos'])
queryClient.setQueryData(
['todos'],
(old) => [...old, newTodo]
)
return { previousTodos }
}
Интерфейс обновляется мгновенно без ожидания сервера.
TanStack Query поддерживает AbortController.
const fetchUsers = async ({ signal }) => {
const response = await fetch('/api/users', {
signal
})
return response.json()
}
При unmount:
Проблема:
Запрос A стартовал
Запрос B стартовал
B завершился
A завершился
Старые данные могут перезаписать новые.
TanStack Query умеет корректно управлять такими ситуациями через:
Suspense уменьшает количество ручной логики:
useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Преимущества:
Плохая практика:
useQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboard
})
Если API возвращает:
{
users,
stats,
notifications,
charts,
messages
}
любое изменение вызывает обновление всего query.
Лучше:
useUsersQuery()
useStatsQuery()
useNotificationsQuery()
Преимущества:
TanStack Query не нормализует данные автоматически.
Плохой вариант:
['posts']
['post', id]
могут хранить разные версии одного объекта.
После mutation:
queryClient.setQueryData(
['post', post.id],
post
)
или:
queryClient.invalidateQueries({
queryKey: ['posts']
})
Избыточная invalidation — одна из самых частых проблем.
Плохо:
queryClient.invalidateQueries()
Это может вызвать:
Лучше:
queryClient.invalidateQueries({
queryKey: ['posts']
})
или:
queryClient.invalidateQueries({
exact: true,
queryKey: ['post', id]
})
TanStack Query умеет группировать обновления.
Несколько изменений кеша:
queryClient.setQueryData(...)
queryClient.setQueryData(...)
queryClient.invalidateQueries(...)
могут быть объединены в один цикл уведомлений.
Это снижает количество ререндеров React.
Кеш можно сохранять:
persistQueryClient({
queryClient,
persister
})
Преимущества:
Слишком большой кеш вызывает:
Важно:
Во время SSR данные могут быть подготовлены заранее.
dehydrate(queryClient)
На клиенте:
hydrate(queryClient, dehydratedState)
Это устраняет:
Даже при использовании TanStack Query лишние ререндеры возможны.
export default React.memo(UserCard)
особенно полезен для:
TanStack Query не решает проблему огромных DOM-деревьев.
Для больших списков необходима виртуализация:
Плохой queryKey:
queryKey: ['users', filters]
если filters создается заново:
{
sort: 'name'
}
на каждом рендере.
Это приводит к:
Лучше:
const filters = useMemo(() => ({
sort: 'name'
}), [])
Каждый useQuery создает observer.
Observer отслеживает:
Большое количество observers увеличивает нагрузку.
Плохо:
items.map(item => (
<Row key={item.id} id={item.id} />
))
если внутри каждого Row:
useQuery(...)
При сотнях элементов появляются:
Иногда эффективнее загрузить данные заранее:
useQuery({
queryKey: ['rows'],
queryFn: fetchRows
})
и передавать их вниз через props.
TanStack Query Devtools:
В production Devtools обычно отключаются.
При анализе TanStack Query отслеживаются:
refetchOnMount: true
refetchOnWindowFocus: true
staleTime: 0
queryKey: ['posts', {}]
GET /all-data
invalidateQueries()
refetchInterval: 1000
GET /posts
для десятков тысяч записей.
Обычно эффективная архитектура включает:
staleTime;gcTime;Именно комбинация этих механизмов позволяет TanStack Query обслуживать крупные приложения с тысячами запросов и сложным UI без критической деградации производительности.