Cache-Control заголовки: полный гид по кэшированию
Заголовок Cache-Control — главный инструмент управления кэшированием в HTTP. Правильная настройка сокращает загрузку сервера, ускоряет сайт для пользователей и улучшает PageSpeed анализ. Неправильная — приводит к «залипшему» контенту, пропаже обновлений и SEO-проблемам.
В этом гайде разберём все директивы Cache-Control, отношения с ETag, Last-Modified и Vary, стратегии для разных типов контента (статика, HTML, API документацию), примеры настройки в nginx и Apache.
Что такое Cache-Control
По RFC 9111, Cache-Control управляет поведением кэшей (браузер, CDN, прокси). Это стандарт с 1999 года (HTTP/1.1), заменивший устаревшие заголовки Expires и Pragma.
Ключевые директивы
max-age=N
Срок жизни кэша в секундах. max-age=3600 = кэш валиден 1 час.
s-maxage=N
Как max-age, но только для shared cache (CDN, reverse proxy), игнорируется браузером.
public
Разрешено кэширование и shared (CDN), и private (браузер).
private
Только браузер. Запрещает CDN кэшировать (для авторизованного контента).
no-cache
НЕ означает «не кэшировать»! Означает «перед использованием кэша всегда проверь у origin через ETag/If-Modified-Since». Вернулся 304 — используй кэш.
no-store
Вот это «совсем не кэшировать». Ни браузер, ни CDN, ни прокси не должны сохранять ответ.
must-revalidate
Использование кэша после истечения запрещено без revalidation. Полезно для финансовых данных.
immutable
Обещание: контент никогда не изменится. Браузер даже не ревалидирует при reload.
stale-while-revalidate=N
Разрешает отдавать устаревший кэш N секунд, пока в фоне идёт ревалидация. Мгновенный response + свежий контент следующим запросом.
stale-if-error=N
Если origin упал, отдавай устаревший кэш N секунд. Защита от downtime.
Типичные паттерны
Статичные ассеты с хэшем в имени
Cache-Control: public, max-age=31536000, immutable
# 1 год, immutable — webpack bundle app.a3f2b.js не изменитсяHTML-страницы
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
# Браузер: всегда проверяет
# CDN: кэш 1 час
# При истечении — stale-while-revalidate на деньAPI endpoints (динамика)
Cache-Control: private, max-age=60
# Личный кэш 60 секунд, CDN не кэшируетАвторизованный контент
Cache-Control: private, no-cache
Vary: Authorization, CookieЧувствительные данные (банк, пароли)
Cache-Control: no-store
Pragma: no-cacheНастройка в nginx
server {
# Статика с хэшем — длинный immutable кэш
location ~* \.(css|js|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Изображения без хэша — умеренный кэш
location ~* \.(png|jpg|gif)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# HTML — минимум кэша, stale-while-revalidate
location ~* \.html$ {
expires 0;
add_header Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400";
}
# API — приватно, коротко
location /api/ {
add_header Cache-Control "private, max-age=60";
proxy_pass http://backend;
}
# Чувствительное
location /api/account {
add_header Cache-Control "no-store";
}
}Настройка в Apache (.htaccess)
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 7 days"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.html$">
Header set Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400"
</FilesMatch>
</IfModule>ETag и Last-Modified: ревалидация
Когда кэш истекает, браузер делает conditional request:
GET /page.html
If-None-Match: "a3f2b"
If-Modified-Since: Wed, 01 Jan 2026 12:00:00 GMTСервер отвечает 304 Not Modified (пустое тело) или 200 с новым контентом. 304 — дешевле, чем полная передача.
# nginx автоматически отдаёт ETag для статики
etag on;
# PHP
$etag = md5_file($path);
header("ETag: \"{$etag}\"");
if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === "\"{$etag}\"") {
http_response_code(304);
exit;
}Vary — учёт различных представлений
Vary: Accept-Encoding
# Разные кэши для gzip/br/identity
Vary: Accept-Language
# Разные кэши для ru/en версий
Vary: Cookie
# Ломает shared cache — используйте только когда необходимоCache-Busting стратегии
- Fingerprinting (хэш в имени файла) —
app.a3f2b.js. Самый надёжный. - Query string —
app.js?v=2. Работает, но некоторые CDN игнорируют query. - Versioning path —
/v2/app.js. Полная инвалидация при релизе. - ETag-based — только для HTML, не для статики.
CDN-специфика
- Cloudflare уважает Cache-Control, но имеет свой Edge TTL в Page Rules. s-maxage не всегда работает — проверяйте через
CF-Cache-Statusheader. - Fastly использует
surrogate-controlдля CDN-only TTL. - AWS CloudFront уважает s-maxage.
Проверка кэширования
Используйте HTTP Header Checker — он покажет Cache-Control, ETag, Last-Modified, Age и CF-Cache-Status в одном месте. Также полный гид по кэшированию содержит продвинутые примеры.
# Проверка из терминала
curl -I https://example.com/app.js
# С ревалидацией
curl -H 'If-None-Match: "a3f2b"' https://example.com/app.js
# ждёте HTTP/2 304Антипаттерны
- no-cache для статики с хэшем — теряете перф, ничего не выигрываете.
- max-age=0 без ETag — полный retransmission на каждый запрос.
- public для авторизованного контента — утечка через CDN.
- Vary: * — полностью ломает кэш.
- Cache-Control без единицы измерения (
max-age=1 year) — только секунды.
Часто задаваемые вопросы (FAQ)
В: Почему no-cache всё равно кэширует?
О: no-cache ≠ no-store. no-cache разрешает хранить, но требует ревалидации. Для полного отключения — no-store.
В: max-age или Expires — что лучше?
О: max-age. Expires — устаревший HTTP/1.0 header, требует правильное время сервера. Cache-Control приоритетнее.
В: Кэш пропадает после обновления сайта?
О: Используйте fingerprinting — новый хэш в имени файла даст новый URL, и браузер загрузит свежую версию.
В: Влияет ли кэширование на SEO?
О: Косвенно через Core Web Vitals. Правильный кэш снижает TTFB и улучшает LCP.
Заключение
Cache-Control — мощный инструмент, но требует понимания директив. Статика с хэшем = immutable на год. HTML = короткий max-age + stale-while-revalidate. Авторизованное = private или no-store. Всегда проверяйте реальные заголовки через Enterno.io HTTP Checker после изменений.
Проверьте ваш сайт прямо сейчас
Проверить →