SWR vs TanStack Query: где разница и что брать в 2026
Раз в год кто-то приходит ко мне в личку и спрашивает: «Юля, что брать — SWR или TanStack Query». И раз в год я отвечаю одно и то же: зависит от проекта. Дальше выясняется, что человек хочет короткий ответ. Короткий ответ есть, но он скучный — «бери TanStack Query, если в проекте есть мутации, бери SWR, если только чтения». Длинный ответ полезнее.
Я работала с обеими библиотеками в проде на разных проектах. Расскажу, в чём реальные различия, а не те, что в README на главной странице.
Что у них общего
На уровне «получить данные с сервера и закешировать» обе библиотеки решают одну задачу одинаково хорошо. Оба:
- Кешируют ответы по ключу.
- Делают stale-while-revalidate: показывают кеш, в фоне догружают свежее.
- Дедуплицируют запросы.
- Перезагружают данные при возврате на вкладку и при переключении фокуса.
- Работают с пагинацией и бесконечной прокруткой.
Если у тебя проект, где данные только читаются (блог, документация, лента), — обе подходят. Разница начинается на мутациях, оптимистичных апдейтах и кеш-менеджменте.
Размер и API
SWR — около 4 КБ minified+gzipped. TanStack Query — около 13 КБ. Разница есть, но в 2026 редко бывает критической. На LCP-критичных страницах с маленьким бандлом SWR помогает, на больших приложениях лишние 9 КБ незаметны.
API SWR минималистичный:
import useSWR from "swr";
const { data, error, isLoading, mutate } = useSWR(
"/api/user",
fetcher,
);
API TanStack Query более структурный:
import { useQuery } from "@tanstack/react-query";
const { data, error, isLoading } = useQuery({
queryKey: ["user"],
queryFn: () => fetch("/api/user").then((r) => r.json()),
staleTime: 60_000,
});
На SWR быстрее писать первые экраны. На TanStack Query больше явных параметров, и дольше пишешь, но потом проще поддерживать.
Различие №1: ключи запросов
В SWR ключ — это строка или массив, который ты руками склеиваешь. В TanStack Query ключ — массив с типизированной структурой, удобной для иерархических инвалидаций:
// TanStack Query
useQuery({ queryKey: ["posts", { author: "yulia" }] });
queryClient.invalidateQueries({ queryKey: ["posts"] }); // инвалидирует все queryKey, начинающиеся с ["posts"]
В SWR пришлось бы писать руками регулярку или итерироваться по кешу. На больших проектах с группами связанных запросов это превращается в боль. У меня на одном проекте поверх SWR появилась обёртка, которая фактически переписывала эту логику. Это был сигнал, что SWR здесь не подходит.
Различие №2: мутации
В SWR мутаций как отдельной концепции нет. Ты делаешь запрос напрямую, потом вручную дёргаешь mutate для обновления кеша:
await fetch("/api/posts", { method: "POST", body: JSON.stringify(data) });
mutate("/api/posts");
Для оптимистичных апдейтов есть useSWRMutation, но API менее удобный, чем у конкурента.
В TanStack Query мутация — отдельный хук с лайфциклом:
const mutation = useMutation({
mutationFn: (data: PostInput) => api.createPost(data),
onMutate: async (newPost) => {
await qc.cancelQueries({ queryKey: ["posts"] });
const prev = qc.getQueryData(["posts"]);
qc.setQueryData(["posts"], (old) => [...(old ?? []), newPost]);
return { prev };
},
onError: (_err, _newPost, ctx) => {
if (ctx?.prev) qc.setQueryData(["posts"], ctx.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: ["posts"] });
},
});
Поэтапные хуки onMutate, onError, onSettled с возможностью передать контекст — на серьёзных проектах это экономит часы дебага. Можно вкорячить тот же сценарий и в SWR, но в TanStack Query это первоклассная фича.
Различие №3: DevTools
У TanStack Query DevTools на голову лучше. Видно весь кеш, статусы запросов, можно вручную инвалидировать или менять данные. На SWR DevTools тоже есть, но беднее.
На проектах, где много запросов и сложные взаимосвязи, DevTools TanStack Query сэкономили мне столько времени, что одного этого хватило бы, чтобы их выбрать.
Различие №4: интеграция с серверным рендером
Обе работают с SSR/RSC, но по-разному.
В Next.js App Router у TanStack Query есть прямая поддержка через HydrationBoundary: на сервере префетчишь данные, сериализуешь состояние клиента, на клиенте оно дегидрируется без повторного запроса.
// app/posts/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
export default async function PostsPage() {
const qc = new QueryClient();
await qc.prefetchQuery({
queryKey: ["posts"],
queryFn: () => api.getPosts(),
});
return (
<HydrationBoundary state={dehydrate(qc)}>
<PostsList />
</HydrationBoundary>
);
}
На SWR такой готовой интеграции нет. Можно передать fallback через провайдер, но это менее структурно.
Различие №5: суспенс-режим
В TanStack Query есть отдельный хук useSuspenseQuery, который интегрируется с React Suspense. На фронте, где ты активно используешь Suspense-границы, это удобно: лоадер не пишешь руками в каждом компоненте, делегируешь его выше.
SWR тоже умеет в suspense-режим через флаг, но мне с ним было сложнее — больше странных поведений с гидрацией и SSR.
Когда я выбираю SWR
- Маленький проект, где основная задача — показать данные.
- Лендинг или сайт-визитка с парой динамических кусков.
- Embed-виджет, где важна каждая килобайта бандла.
- Команда, которая впервые работает с server-state библиотекой и не готова осваивать сложный API.
Когда я выбираю TanStack Query
- Любой дашборд или админка, где есть мутации.
- Проект с оптимистичными апдейтами и сложной инвалидацией.
- Приложение с offline-сценариями (TanStack Query умеет персистить кеш и приостанавливать мутации).
- Команда, которая будет долго поддерживать проект и хочет предсказуемой структуры.
Что не делаю никогда
Не использую обе библиотеки в одном проекте. Видела такое — половина команды на SWR, половина на TanStack Query. В итоге кэши разные, инвалидация рассогласована, после мутации одна часть UI обновляется, другая нет. Не повторяй.
Не использую ни одну из них для глобального UI-стейта. Эти библиотеки про серверные данные. UI-стейт — отдельная история (об этом есть отдельная статья).
Не пытаюсь хранить в кеше выведенные данные (например, отфильтрованный список). Кеш — про сырые ответы сервера. Производные данные считай в рендере или через useMemo.
Замер на реальном проекте
На одном из переездов с SWR на TanStack Query (около 30 запросов и 15 мутаций) у нас:
- Бандл вырос на 9 КБ gzipped.
- Количество багов с инвалидацией кеша снизилось примерно вдвое за квартал.
- Время на ревью PR с мутациями стало на 20–30% меньше — структура кода стала очевиднее.
Я не сторонник чистых цифр без контекста, но в этом случае выигрыш был ощутимый. Хотя признаю: на проекте с пятью простыми запросами этот переезд был бы потерянным временем.
Что запомнить
SWR и TanStack Query решают одну задачу с разной плотностью API. Для маленьких проектов SWR быстрее в старте и легче в бандле. Для всего, где есть мутации и сложная инвалидация, TanStack Query сэкономит часы на отладке и принесёт DevTools, ради которых стоило бы взять её даже без других плюсов.
Не выбирай по моде, выбирай по тому, какие сценарии у тебя реально есть в проекте. Если основная задача — показать данные, бери что попроще. Если задача — управлять данными, бери что предсказуемее.