Оглавление
Зачем бизнесу HTTP‑кеширование
Кеш на уровне HTTP — один из самых дешёвых способов ускорить продукт и снизить нагрузку на бэкенд. Он работает прозрачно, не требует миграций баз и изменения бизнес‑логики, а выгода ощутима сразу:
- Ускорение TTFB и общей скорости страницы в 2–5 раз благодаря выдаче ответа с периметра (CDN, обратный прокси).
- Снижение трафика на 40–80% за счёт 304 Not Modified и отдачи из кеша без обращения к приложению.
- Меньше пиковых нагрузок и расходов на инфраструктуру: реже масштабируем бэкенд, реже упираемся в базы.
- Более стабильный SLA: при проблемах на бэкенде можно отдавать слегка устаревшие ответы (stale-if-error), чтобы не падать целиком.
Главное — настроить базовые заголовки, разделить публичные и приватные ответы и аккуратно выбрать стратегию инвалидации.
Что можно кешировать без риска
Безопасные кандидаты:
- Публичные статические файлы: CSS, JS, изображения, шрифты. Идеально — с версионированием по имени файла (хеш в имени).
- Публичные HTML‑страницы, которые одинаковы для всех и меняются не каждую секунду: маркетинговые страницы, статьи, справка, лендинги.
- Публичные API‑ответы, нечувствительные к пользователю и быстро изменяющиеся только по расписанию: курсы валют, списки категорий, конфигурации.
- Результаты тяжёлых вычислений или агрегаций, если для них есть допустимое «окно устаревания» (например, 30–120 секунд).
Осторожно:
- Персонализированные страницы и API с авторизацией. Их либо нельзя кешировать, либо нужно разделять ключи кеша по пользователю и явно ставить Cache-Control: private.
- Ответы с Set-Cookie: они часто случайно отключают кеш на прокси и CDN. Если cookie не критична, отдавайте её с путём и доменом, не мешающими кешу, или убирайте.
Ключевые заголовки: Cache-Control, ETag, Last-Modified, Vary
-
Cache-Control — главный директивный заголовок. Полезные флаги:
- max-age=60 — хранить в браузере/промежуточных кешах до 60 секунд.
- s-maxage=300 — отдельный TTL для прокси/CDN. Удобно давать прокси больше, чем браузеру.
- public — ответ можно кешировать где угодно (если нет авторизации и приватных данных).
- private — кешировать только в браузере пользователя, не в общих прокси.
- no-store — ничего не хранить (для секретных ответов).
- stale-while-revalidate=30 — можно вернуть устаревший ответ, пока в фоне идёт обновление.
- stale-if-error=600 — при ошибке бэкенда можно отдавать устаревший ответ до 10 минут.
-
ETag — «подпись» содержимого. Клиент присылает If-None-Match, сервер сравнивает и может ответить 304, если ничего не изменилось.
-
Last-Modified — дата последнего изменения. Работает с If-Modified-Since.
-
Vary — список заголовков, от которых зависит ответ. Минимизируйте список, иначе кеш может раздробиться на множество вариантов. Обычно хватает Vary: Accept-Encoding, Accept-Language. Не добавляйте Authorization, если не уверены — это почти всегда ломает кеш.
Стратегии кеширования: TTL, revalidate, microcache
- Жёсткий TTL (max-age) — просто и надёжно для статического и медленно меняющегося контента. Изменили — инвалидация по версии файла/пурж по тегу.
- Условная валидация (ETag/Last-Modified) — экономит трафик: бэкенд быстро отвечает 304 и не гонит тело.
- stale-while-revalidate — пользователи получают быстрый ответ, а обновление происходит в фоне. Отлично для страниц, где допустима краткая «устарелость».
- Microcache на периметре (1–5 секунд) — сглаживает всплески нагрузки и «рои» запросов, когда многие клиенты бьют по одному ресурсу.
Кеш на периметре: CDN и обратный прокси
- CDN (например, любой облачный провайдер) — раздаёт контент с ближайших узлов, экономит трафик и ускоряет отдачу. Можно использовать правила по заголовкам, тегам и запросам.
- Обратный прокси (Nginx, Varnish) — ставится перед приложением. Кеширует и защищает бэкенд. Работает в паре с CDN или самостоятельно.
Принцип: бэкенд аккуратно проставляет заголовки, периметр уважает их и кеширует/валидирует. При изменениях выполняется точечная инвалидация (tag/purge) либо выпускается новая версия ресурса.
Практика: конфиг Nginx и пример на Node.js
Nginx: кеш, microcache и условия для 304
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:100m max_size=5g inactive=10m use_temp_path=off;
map $http_accept_language $lang_key {
default "en";
~^ru "ru";
}
server {
listen 80;
server_name example.com;
# Кешируем только безопасные публичные ответы
location /public/ {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Ключ кеша зависит от URI и языка
proxy_cache_key "$scheme://$host$request_uri|$lang_key";
proxy_cache api_cache;
proxy_cache_valid 200 301 302 10m; # базовый TTL
proxy_cache_valid 404 1m; # негативный кеш (осторожно)
# Разрешаем отдавать устаревший ответ, пока идёт фоновое обновление
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# Уважать ETag/Last-Modified от бэкенда
proxy_set_header If-None-Match $upstream_http_etag;
proxy_set_header If-Modified-Since $upstream_http_last_modified;
add_header X-Cache-Status $upstream_cache_status;
}
# Microcache для API, сильно бьющих по базе
location /api/expensive/ {
proxy_pass http://app;
proxy_cache api_cache;
proxy_cache_valid 200 5s;
proxy_cache_use_stale updating;
add_header X-Cache-Status $upstream_cache_status;
}
}
Пояснения:
- proxy_cache_valid задаёт TTL на периметре. Для браузеров TTL настраивается бэкендом в Cache-Control.
- proxy_cache_use_stale и proxy_cache_background_update дают поведение stale-while-revalidate на стороне Nginx.
- X-Cache-Status помогает быстро видеть HIT/BYPASS/MISS в логах и APM.
Node.js (Express): ETag, Cache-Control и «304 без тела»
Express по умолчанию умеет ETag, но для демонстрации сделаем явно и добавим корректные заголовки.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
function weakETag(buf) {
const hash = crypto.createHash('sha1').update(buf).digest('hex');
return `W/"${hash}"`;
}
app.get('/public/article/:id', async (req, res) => {
const article = {
id: req.params.id,
title: 'HTTP-кеширование',
updatedAt: new Date('2024-11-10T10:00:00Z')
};
const body = Buffer.from(JSON.stringify(article));
const etag = weakETag(body);
const lastModified = article.updatedAt.toUTCString();
if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === lastModified) {
res.status(304);
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300, stale-while-revalidate=30, stale-if-error=600');
return res.end();
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
res.setHeader('Vary', 'Accept-Encoding, Accept-Language');
res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300, stale-while-revalidate=30, stale-if-error=600');
res.send(body);
});
app.listen(3000, () => console.log('Server on http://localhost:3000'));
Проверка 304:
curl -i http://localhost:3000/public/article/42
curl -i http://localhost:3000/public/article/42 -H 'If-None-Match: W/"<хеш>"'
Аутентификация и приватные ответы
Главное правило: не кэшировать то, что персонально для пользователя, в общих прокси/CDN. Практики:
- Для приватных ответов ставьте Cache-Control: private, no-store или private, max-age=0, must-revalidate.
- Не добавляйте Authorization в Vary, если вы не реализуете отдельный ключ кеша для каждого токена. Проще полностью отключить общий кеш для таких ответов.
- Персонализацию лучше делать на клиенте или через встраивание «лёгких» элементов, не мешающих кешу основного HTML (например, основной HTML кэшируем, блок с именем пользователя грузим отдельным приватным запросом).
- Если всё же нужен общий кеш, используйте разделение по ключам (например, X-User-Segment) и разрешайте кеш только для безопасных сегментов, где нет персональных данных.
Инвалидация: теги, purge и безопасные обходные пути
Инвалидация бывает двух типов:
- По времени (TTL) — дёшево и просто. Подходит для большинства публичных страниц.
- Явная (purge) — когда нужно снять из кеша конкретные ресурсы: после публикации, правки цен, срочных новостей. Удобно использовать теги:
- В ответ добавляем заголовок с тегом (например, Surrogate-Key: article, category).
- В CDN или прокси выполняем purge по тегу, не перечисляя все URL.
Если теги недоступны:
- Версионирование в путях: /assets/app.abc123.js. При выпуске новой версии URL меняется, и кеш автоматически «обновляется».
- Переотдача ETag/Last-Modified: при обновлении содержимого меняется подпись — клиенты получают свежие данные.
Важно: доступ к purge должен быть строго ограничён по сети и ключам, иначе возможны злоупотребления и случайные массовые очистки.
Метрики и контроль качества
Следите за:
- Hit ratio на CDN/прокси (доля HIT к общим запросам). Хорошая цель — 60–90% на статике, 30–70% на HTML/API в зависимости от природы данных.
- Доли 304 Not Modified — индикатор, что условная валидация работает.
- TTFB и p95/p99 задержек до и после включения кеша.
- Ошибок 5xx с флагами stale-if-error — помогает понимать, как периметр сглаживает сбои бэкенда.
- Размеров кеша, количества ключей и частоты инвалидации. Слишком частый purge может съедать выгоду.
Типичные ошибки и как их избежать
- Случайный запрет кеша из‑за Set-Cookie. Проверьте, действительно ли cookie нужна на публичном ресурсе. Часто её можно ограничить по пути или убрать.
- Огромный Vary. Чем больше заголовков в Vary, тем сильнее дробится кеш. Используйте только действительно влияющие: Accept-Encoding, Accept-Language.
- Кеширование приватного. Никогда не кешируйте вместе публичный и приватный контент. Для приватного — private/no-store.
- Бессрочные 301/302 без контроля. Редиректы тоже кешируются. Ставьте разумный TTL и обновляйте при изменениях.
- Негативный кеш 404 на долгий срок. Иначе баг в маршрутизации превратит сайт в «минное поле». Ограничивайте 404 минутами, а не часами.
- Инвалидация «молотком». Массовый purge часто бьёт по производительности. Используйте теги и версионирование.
- Cache poisoning: злоумышленник может принести необычные заголовки и «отравить» кеш. Жёстко нормализуйте заголовки, ограничивайте Vary и фильтруйте запросы на периметре.
Пошаговый план внедрения за 2 недели
Неделя 1:
- Аудит: список публичных маршрутов, статических файлов, API без персональных данных.
- Включить ETag и Last-Modified на публичных HTML/API. Проверить 304 через curl и браузерные инструменты.
- Статика: добавить хеши в имена файлов, выставить Cache-Control: public, max-age=31536000, immutable.
- На периметре включить proxy_cache с microcache для тяжёлых API (5 секунд) и базовые правила для /public/.
Неделя 2:
- Добавить s-maxage и stale-while-revalidate/stale-if-error для HTML и публичных API.
- Отстроить Vary: оставить только действительно влияющие заголовки.
- Внедрить метрики: X-Cache-Status в логи, графики HIT/MISS, 304, TTFB.
- Настроить аккуратную инвалидацию: теги или версионирование. Ограничить доступ к purge.
- Прогнать нагрузочные тесты и проверить, что при сбоях бэкенда периметр подстраховывает, а не ломает выдачу.
Результат: быстрые страницы, меньшая нагрузка на бэкенд и прогнозируемые затраты на инфраструктуру — без масштабных переписываний.