В современном стеке разработки кэширование HTTP-ответов кажется рутиной. Добавляем Cache-Control: max-age=3600
, и можно переходить к следующим задачам, верно? Но именно эта иллюзия "простоты" приводит к тонким багам, чрезмерной нагрузке на серверы и разочарованным пользователям. Разберёмся, как настроить кэширование, избегая подводных камней.
Почему max-age ≠ "работает как надо"
Типичное заблуждение: время жизни контента (max-age
) — единственный параметр, который имеет значение. В реальности поведение кэширования зависит от цепочки устройств: CDN, инверсные прокси, браузеры — каждый может трактовать заголовки по-своему. Рассмотрим опасный пример:
Cache-Control: public, max-age=3600
Кажется безопасным? Теперь представьте ответ API с приватными данными пользователя, который случайно помечен как public
. CDN может отдать эти данные другому клиенту. Реальная авария подобного рода случилась с EU VAT API в 2020 году.
Исправление:
- Для персонализированных данных всегда используйте
private
:httpCache-Control: private, max-age=600
- Для общедоступного контента явно указывайте
public
:httpCache-Control: public, max-age=86400
Забытый Vary: Как кэш ломает ваш адаптивный дизайн
Предположим, ваш бэкенд возвращает разметку адаптивной страницы для мобильных и десктопных устройств через user-agent. Без указания Vary
кэш может сохранить одну версию и отдать её всем клиентам. Результат: мобильные пользователи видят кривой интерфейс.
Правильное решение:
Cache-Control: public, max-age=3600
Vary: User-Agent
Но есть нюанс: множество уникальных User-Agent-строк быстро переполнит кэш. Альтернатива:
- Используйте серверную логику для группировки устройств (mobile/desktop/tablet)
- Передавайте тип устройства в URL (
/data?platform=mobile
) - Устанавливайте
Vary
для заголовка, который вы определяете сами (например,X-Device-Group
)
Must-revalidate и мутации данных
Когда срок действия кэша истёк, сервер по умолчанию может отдать устаревшие данные во время повторной валидации (conditional GET). Если у вас абсолютно критические данные (например, расписание авиарейсов), это неприемлемо.
Директива must-revalidate
блокирует отдачу устаревшего контента в любых условиях:
Cache-Control: public, max-age=300, must-revalidate
Но помните о рисках: при массовом обращении после истечения max-age сервер получит лавину запросов для revalidate. Решение: комбинировать с stale-while-revalidate
, разрешающим кратковременное использование устаревших данных во время фоновой валидации.
Онлайновые вычисления: Когда кэш бессилен
Ваши данные меняются каждые 10 секунд? Не пытайтесь кэшировать их с max-age=10
. Реальная инфраструктура игнорирует малые значения TTL из-за сетевых задержек.
Рабочий паттерн:
- Установите
no-cache
с валидацией через ETag:httpCache-Control: no-cache ETag: "a3b79f4"
- Браузер будет отправлять запросы каждый раз, но при неизменном ETag сервер возвращает 304 (Not Modified) без тела ответа.
- Снижаете нагрузку на сеть, сохраняя актуальность.
Кэширование ошибочных ответов
Катастрофическая ошибка: кэширование 5xx ошибок. Клиент получит "500 Internal Server Error" в течение всего max-age, даже если сервер уже восстановился.
Защита:
# Конфиг Nginx
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid 500 5s; # Крайне малый TTL для ошибок
Или в заголовках приложения (не все CDN поддерживают):
Cache-Control: public, max-age=600, no-cache=status-code-500
Тестирование и интроспекция
Как проверить, что кэш работает как задумано? Стронгонегирайте:
-
cURL с проверкой заголовков
curl -v -H "Cache-Control: no-cache" http://example.com
– проигнорирует кэш при наличииcurl -v http://example.com
– использует кэш по умолчанию -
Chrome DevTools/Network
Смотрите колонки "Size" (disk cache/memory cache), статусы 200 vs 304 -
Серверный логический датчик
Добавьте заголовокX-Cache-Status: Miss/Hit/Expired
, чтобы отслеживать решения кэша
Стратегия вместо кусочных решений
Кэширование — это архитектурное решение. Единых правил для всех эндпоинтов нет. Схема для REST API:
-
Постоянные данные (например, геокоды):
public, max-age=31536000, immutable
(даже при сбое сети данные не изменены) -
Персонализированный контент (профиль пользователя):
private, max-age=60, stale-while-revalidate=86400
-
Часто меняющиеся данные (список заказов):
no-cache
с ETag -
Критически важные (оплата):
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
Кэширование — не просто заголовок в ответе сервера. Это договорённость между системными компонентами, которая требует инженерной дисциплины. Нарушение правил кажется дешёвым способом ускорить разработку, но расплатой будут снижение отказоустойчивости, утечки данных и больше ночных аварийных вызовов. Ваш кэш обязан работать как швейцарские часы: молча, незаметно и точнее хронометра.