Учебник веб-разработки
Разделы учебника
На этой странице

VI. HTTP, Next.js и кеширование

Кеширование: ключи, свежесть и инвалидирование

Оглавление · Способы рендеринга · SSG/ISR проекта

Задача: определить, что можно переиспользовать, как долго и после каких изменений результат больше не подходит. Нужны отображения, HTTP как обмен запросом/ответом и различие источника и представления. Redis для этого разбора не требуется; его роль в хранении будет рассмотрена отдельно.

Ключ задаёт эквивалентность запросов

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

Для render(news, language) ключ только из news.id потеряет язык. Для персонального ответа ключ только из URL может смешать разные области доступа. Сначала задайте эквивалентность входов, затем выбирайте формат ключа. Математический образ: ключ разбивает запросы на классы; внутри класса результат должен подходить каждому допустимому запросу.

Кеш не становится источником истины только потому, что читает быстрее. Изменение источника не изменяет уже сохранённую копию. Нужны срок свежести, инвалидирование или проверка версии.

Срок свежести и срок хранения

Hit означает найденную пригодную запись, miss — необходимость получить результат из источника. TTL задаёт срок в выбранной модели, вытеснение освобождает память, инвалидирование запрещает использование после значимого события. Это разные причины.

Истечение TTL не обязано сразу удалять запись физически или запускать таймер. Вычисление может происходить при следующем обращении. Устаревшая запись иногда остаётся для контролируемой выдачи при обновлении, но это отдельная политика.

export function createLessonCache<T>(load: (key: string) => T, now: () => number, ttl: number) {
  const entries = new Map<string, { value: T; expiresAt: number }>();
  return {
    read(key: string): T {
      const time = now();
      const entry = entries.get(key);
      if (entry && time < entry.expiresAt) return entry.value;
      const value = load(key);
      entries.set(key, { value, expiresAt: time + ttl });
      return value;
    },
    invalidate(key: string) {
      entries.delete(key);
    },
  };
}

let time = 0;
let title = "Первый заголовок";
let reads = 0;
const cache = createLessonCache(
  () => {
    reads++;
    return title;
  },
  () => time,
  10,
);
console.log(cache.read("news:sample")); // Первый заголовок
title = "Новый заголовок";
console.log(cache.read("news:sample")); // Первый заголовок: запись ещё свежая
cache.invalidate("news:sample");
console.log(cache.read("news:sample")); // Новый заголовок
console.log(reads); // 2

Это синхронная учебная модель с ручными часами: конечное монотонное время, положительный конечный TTL, согласованные единицы измерения. Ровно в expiresAt запись уже просрочена. Исключение load не сохраняется как успешный результат. Входные ключи должны правильно разделять источники и варианты.

Модель не ограничивает размер, не очищает забытые ключи, не копирует объекты результата и не объединяет параллельные async-чтения. Её нельзя выдавать за production-cache handler. Строковый пример нужен, чтобы проверить правила без сети и реальных часов.

Уровни не отменяют друг друга автоматически

Исходник схемы
flowchart LR
  Browser["Браузер: локальные кеши"] --> Proxy["HTTP-прокси / CDN, если настроен"]
  Proxy --> Route["Приложение: результат маршрута"]
  Route --> Data["Приложение: данные источника"]
  Data --> Source["Источник истины"]

Стрелки — возможный путь чтения при промахах, не обязательные компоненты Atmanki. Прокси не становится CDN-кешем только из-за наличия reverse_proxy. Можно обновить серверные данные, но продолжать читать старое представление на другом уровне. Для каждой копии нужны владелец, ключ и политика свежести.

HTTP-кеш и условный запрос

Cache-Control описывает правила HTTP-переиспользования. max-age задаёт срок свежести; private запрещает хранение в общем кеше; no-cache требует проверки перед повторным использованием; no-store запрещает хранение по этому механизму. no-cache не означает «не хранить». RFC 9111.

ETag — валидатор представления. Для GET клиент может передать If-None-Match; если представление совпадает, ответ 304 позволяет использовать сохранённое тело. 304 не является новым пустым документом. Условные запросы.

Vary учитывает названные поля запроса при выборе представления из HTTP-кеша. Он не заменяет проверку полномочий и не исправляет неверный внутренний ключ. Ключи HTTP-кеша. Расширение stale-while-revalidate допускает ограниченную выдачу старого ответа параллельно проверке свежести; оно не обещает мгновенное получение новой версии. RFC 5861.

HTTP-заголовок ответа браузеру и next.revalidate серверного fetch относятся к разным механизмам. Одинаковые слова «кеш» и «ревалидация» не делают их одной записью.

Кеши текущей модели Next

В проекте Cache Components не включены. Рассматриваем модель с явными fetch-настройками, соответствующую текущей конфигурации. Кеширование без Cache Components.

МеханизмЧто переиспользуетсяПример Atmanki
Мемоизация React cacheРезультат вызова в области серверного рендераОбщие чтения getSite для layout и metadata
Data CacheДанные чтений между обращениямиfetch CMS с force-cache, тегом cms и сроком
Full Route CacheHTML/RSC статического маршрутаПодготовленная новость
Router CacheДанные навигации в браузереПовторное использование сегментов клиентским роутером

React cache сам по себе не является межпользовательским долговременным хранилищем. Его обёртки в queries.ts объявлены на уровне модуля, а не заново при каждом вызове. Для разных slug используются разные аргументы.

В CMS-клиенте force-cache, revalidate: 3600 и tags: ["cms"] указаны явно. У content-health другой смысл: cache:false даёт no-store чтение, а HTTP-ответ также помечен no-store. Готовая страница не подтверждает живую доступность CMS.

Сигнал обновления и получение результата

Webhook вызывает revalidateTag("cms", { expire: 0 }) и revalidatePath("/", "layout"). Первый вызов делает данные тега просроченными для следующего чтения, второй помечает затронутые маршруты для обновления. Обработчик не обходит все URL с новым рендером. revalidateTag, revalidatePath.

Исходник схемы
sequenceDiagram
  participant CMS as Strapi
  participant Web as Next.js
  participant Reader as Следующий посетитель
  CMS->>Web: Уведомление об изменении
  Web->>Web: Истечение данных и инвалидирование пути
  Web-->>CMS: Уведомление принято
  Reader->>Web: Запрос страницы
  Web->>CMS: Чтение актуальных опубликованных данных
  CMS-->>Web: Данные
  Web-->>Reader: Обновлённое представление

Это успешный сценарий, не гарантия доставки webhook. Потерянное уведомление, отказ CMS или старый клиентский снимок требуют отдельной проверки. Срок по времени — страховочная политика, не расписание обхода сайта. router.refresh() обновляет текущий клиентский маршрут, но сам по себе не инвалидирует серверный Data Cache. useRouter.

Гонки и границы

Несколько промахов одного ключа могут одновременно нагрузить источник — cache stampede. Слияние одинаковой работы, ограничение конкурентности и разброс сроков помогают в разных сценариях, но не следуют из наличия Map или Redis.

Если старое чтение началось до инвалидирования и завершилось после нового, оно может снова записать старую версию. Нужна политика версий, поколения кеша или другой механизм согласованности. Простой delete не доказывает отсутствие гонки. При нескольких экземплярах приложения локальный Map одного процесса не инвалидирует копии соседей.

У Atmanki один Next-сервер; Redis и CDN-кеш здесь не заявлены. Масштабирование и общий cache handler — будущая инфраструктурная задача, не эффект смены output или добавления ещё одного контейнера.

Практика и самопроверка

Сохраните TS-блок как временный lesson.mts и запустите Node. Проверьте hit без повторного load, разные ключи, истечение ровно при time=10, инвалидирование и ошибку load. Измените источник без инвалидирования и объясните старый результат. Удалите временный файл после упражнения.

Нарисуйте уровни для новости и для content-health. Для каждого укажите, какое свидетельство проверяет скрипт SSG/ISR. x-nextjs-cache: HIT характеризует конкретный слой и запрос, а не все кеши сайта. Не запускайте сценарии изменения на живой CMS ради учебной проверки.

Объясните, почему новый fetch, новый HTML и новая открытая вкладка — разные этапы, а no-cache и no-store не являются взаимозаменяемыми настройками.