Мастерство HTTP-кэширования: Как избежать скрытых проблем с Cache-Control

В современном стеке разработки кэширование HTTP-ответов кажется рутиной. Добавляем Cache-Control: max-age=3600, и можно переходить к следующим задачам, верно? Но именно эта иллюзия "простоты" приводит к тонким багам, чрезмерной нагрузке на серверы и разочарованным пользователям. Разберёмся, как настроить кэширование, избегая подводных камней.


Почему max-age ≠ "работает как надо"

Типичное заблуждение: время жизни контента (max-age) — единственный параметр, который имеет значение. В реальности поведение кэширования зависит от цепочки устройств: CDN, инверсные прокси, браузеры — каждый может трактовать заголовки по-своему. Рассмотрим опасный пример:

http
Cache-Control: public, max-age=3600

Кажется безопасным? Теперь представьте ответ API с приватными данными пользователя, который случайно помечен как public. CDN может отдать эти данные другому клиенту. Реальная авария подобного рода случилась с EU VAT API в 2020 году.

Исправление:

  • Для персонализированных данных всегда используйте private:
    http
    Cache-Control: private, max-age=600
    
  • Для общедоступного контента явно указывайте public:
    http
    Cache-Control: public, max-age=86400
    

Забытый Vary: Как кэш ломает ваш адаптивный дизайн

Предположим, ваш бэкенд возвращает разметку адаптивной страницы для мобильных и десктопных устройств через user-agent. Без указания Vary кэш может сохранить одну версию и отдать её всем клиентам. Результат: мобильные пользователи видят кривой интерфейс.

Правильное решение:

http
Cache-Control: public, max-age=3600
Vary: User-Agent

Но есть нюанс: множество уникальных User-Agent-строк быстро переполнит кэш. Альтернатива:

  1. Используйте серверную логику для группировки устройств (mobile/desktop/tablet)
  2. Передавайте тип устройства в URL (/data?platform=mobile)
  3. Устанавливайте Vary для заголовка, который вы определяете сами (например, X-Device-Group)

Must-revalidate и мутации данных

Когда срок действия кэша истёк, сервер по умолчанию может отдать устаревшие данные во время повторной валидации (conditional GET). Если у вас абсолютно критические данные (например, расписание авиарейсов), это неприемлемо.

Директива must-revalidate блокирует отдачу устаревшего контента в любых условиях:

http
Cache-Control: public, max-age=300, must-revalidate

Но помните о рисках: при массовом обращении после истечения max-age сервер получит лавину запросов для revalidate. Решение: комбинировать с stale-while-revalidate, разрешающим кратковременное использование устаревших данных во время фоновой валидации.


Онлайновые вычисления: Когда кэш бессилен

Ваши данные меняются каждые 10 секунд? Не пытайтесь кэшировать их с max-age=10. Реальная инфраструктура игнорирует малые значения TTL из-за сетевых задержек.

Рабочий паттерн:

  • Установите no-cache с валидацией через ETag:
    http
    Cache-Control: no-cache
    ETag: "a3b79f4"
    
  • Браузер будет отправлять запросы каждый раз, но при неизменном ETag сервер возвращает 304 (Not Modified) без тела ответа.
  • Снижаете нагрузку на сеть, сохраняя актуальность.

Кэширование ошибочных ответов

Катастрофическая ошибка: кэширование 5xx ошибок. Клиент получит "500 Internal Server Error" в течение всего max-age, даже если сервер уже восстановился.

Защита:

nginx
# Конфиг Nginx
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid 500 5s; # Крайне малый TTL для ошибок

Или в заголовках приложения (не все CDN поддерживают):

http
Cache-Control: public, max-age=600, no-cache=status-code-500

Тестирование и интроспекция

Как проверить, что кэш работает как задумано? Стронгонегирайте:

  1. cURL с проверкой заголовков
    curl -v -H "Cache-Control: no-cache" http://example.com – проигнорирует кэш при наличии curl -v http://example.com – использует кэш по умолчанию

  2. Chrome DevTools/Network
    Смотрите колонки "Size" (disk cache/memory cache), статусы 200 vs 304

  3. Серверный логический датчик
    Добавьте заголовок X-Cache-Status: Miss/Hit/Expired, чтобы отслеживать решения кэша


Стратегия вместо кусочных решений

Кэширование — это архитектурное решение. Единых правил для всех эндпоинтов нет. Схема для REST API:

  1. Постоянные данные (например, геокоды):
    public, max-age=31536000, immutable (даже при сбое сети данные не изменены)

  2. Персонализированный контент (профиль пользователя):
    private, max-age=60, stale-while-revalidate=86400

  3. Часто меняющиеся данные (список заказов):
    no-cache с ETag

  4. Критически важные (оплата):
    no-store, must-revalidate (полный запрет кэширования)


Зоны ответственности

Не перекладывайте всё на клиента. Бэкенд обязан:

  • Генерировать корректные Cache-Control, ETag, Vary
  • Поддерживать conditional GET (If-None-Match)
  • Фильтровать невалидные комбинации (например, no-store и max-age несовместимы)
  • Чистить CDN при миграциях через Clear-site-data: cache или API CDN

Инфраструктура фронтенда отвечает за:

  • Контроль кэширования assets (вечный кэш для хаш-именованных файлов)
  • Делегирование решения о ревалидации (SWR, React Query)
  • Анализ хитов/миссов в DevTools

Кэширование — не просто заголовок в ответе сервера. Это договорённость между системными компонентами, которая требует инженерной дисциплины. Нарушение правил кажется дешёвым способом ускорить разработку, но расплатой будут снижение отказоустойчивости, утечки данных и больше ночных аварийных вызовов. Ваш кэш обязан работать как швейцарские часы: молча, незаметно и точнее хронометра.