Клиент: Apollo Client и кэш

На фронтенде запросы отправляет GraphQL-клиент. Apollo Client не просто шлёт запросы — он нормализует ответы в кэш по идентификаторам и отдаёт данные мгновенно при повторе.

В REST за тебя кэширует браузер по URL. В GraphQL URL один — поэтому умный кэш переезжает на клиента, и это его суперсила.

Технически отправить GraphQL-запрос можно обычным fetch — это просто POST с JSON. Но на практике используют клиентские библиотеки: Apollo Client, urql, Relay. Они берут на себя кэш, повторные запросы, обновление UI и работу с переменными. Самый популярный — Apollo Client. Стоит знать и про различия: urql по умолчанию использует более простой документный кэш (ответ кэшируется целиком по паре «запрос + переменные»), тогда как Apollo Client и Relay строят нормализованный кэш по идентификаторам объектов. Документный кэш проще и отлично подходит для несложного CRUD; нормализованный сложнее, но окупается на приложениях с богатыми связями, оптимистичными обновлениями и общими между экранами сущностями. Выбор клиента — это во многом выбор стратегии кэширования под твой тип приложения.

Нормализованный кэш

Главная фишка Apollo Client — нормализованный кэш. Он не хранит ответы как есть, а разбирает их на объекты по ключу __typename + id и складывает в плоский словарь. Поэтому один и тот же пользователь, пришедший в двух разных запросах, в кэше лежит один раз. Обновил его в одном месте — обновилось везде.

   Ответ запроса A          Нормализованный кэш
   user(1) { name }     ->  "User:1": { id:1, name:"Аня" }
   Ответ запроса B          "Post:7": { id:7, title:"...",
   post(7){author{name}} ->            author: ref("User:1") }
                                        ^ ссылка, а не копия!

Когда придёт мутация, обновившая имя User:1, оба места (запрос A и автор поста из запроса B) покажут новое имя — потому что это одна запись в кэше.

Переменные и ключ кэша

Результат запроса кэшируется по документу запроса + переменным. Запрос с id: "1" и тот же запрос с id: "2" — это разные ключи кэша. Поэтому так важно выносить значения в переменные (как мы учили в разделе про запросы): по ним строится ключ кэша.

Как работает под капотом

При запросе клиент сначала проверяет кэш: есть данные под этим ключом — отдаёт мгновенно (cache hit), нет — идёт по сети, кладёт ответ в нормализованный кэш и только потом отдаёт UI. Смоделируем нормализацию и попадание в кэш:

const cache = new Map();

// нормализация: раскладываем объекты по ключу __typename:id
function writeToCache(obj) {
  const key = obj.__typename + ":" + obj.id;
  cache.set(key, { ...cache.get(key), ...obj });
  return key;
}

let networkCalls = 0;
function fetchUser(id) {           // "поход по сети"
  networkCalls++;
  return { __typename: "User", id, name: "Аня" };
}

function getUser(id) {
  const key = "User:" + id;
  if (cache.has(key)) return { from: "cache", data: cache.get(key) };
  const data = fetchUser(id);
  writeToCache(data);
  return { from: "network", data };
}

console.log(getUser(1)); // network — кэш пуст
console.log(getUser(1)); // cache  — мгновенно, без сети!
console.log("Походов по сети:", networkCalls); // 1, а не 2

Попробуй сам ▶ — запроси getUser(2): это новый ключ, будет поход по сети, а затем повтор getUser(2) уже из кэша. Так клиент экономит запросы.

Частые ошибки

  • Ждать, что HTTP-кэш «сам справится». GraphQL — это POST в один URL, браузерный кэш по URL тут бесполезен. Кэшем управляет клиент.
  • Объекты без id. Нормализация опирается на идентификатор. Если у типа нет id, Apollo не сможет нормализовать его и обновления «расползутся».
  • Забыть обновить кэш после мутации. Создал новый объект мутацией — его может не быть в кэше списка; иногда кэш нужно поправить вручную или перезапросить.

Best practices

  • Всегда запрашивай id у объектов, которые должны кэшироваться — это топливо нормализации.
  • Используй переменные: они формируют ключ кэша и делают запросы переиспользуемыми.
  • Для простого CRUD хватает документного кэша (как у urql); нормализованный кэш Apollo окупается на сложных связях, оптимистичных обновлениях и пагинации.

Итоги

GraphQL-клиент (чаще всего Apollo Client) отправляет запросы и хранит ответы в нормализованном кэше по ключу __typename:id: один объект — одна запись, обновил раз — обновилось везде. Ключ кэша строится из запроса и переменных, а HTTP-кэш браузера здесь не работает, потому что эндпоинт один. Дальше соберём всё в финальные best practices.

Проверьте себя
1. Как Apollo Client хранит данные в нормализованном кэше?
AКак сырые ответы целиком
BРазбивает на объекты по ключу __typename:id — один объект хранится один раз
CТолько в localStorage
DВ виде SQL-таблиц
2. Почему обычный HTTP-кэш браузера слабо помогает в GraphQL?
AБраузеры не умеют кэшировать
BЗапросы идут POST в один общий URL, и кэш по URL бесполезен
CGraphQL запрещает кэш
DОтветы слишком большие