Если вы хотите, чтобы сайт открывался «из памяти браузера» и не тратил лишние запросы, вам нужны корректные заголовки для статики и дисциплина при выкладке. В этой статье разберём, как согласовать cache-control
, etag
, immutable
, expires
, когда и на каких типах файлов их использовать, а также как внедрить грамотное versioning статических ассетов. Покажу рабочие примеры для nginx
и apache
, включая сценарии для virtual hosting и типичные подводные камни.
Что именно кэшировать и как это работает
Под «статикой» чаще всего понимают CSS/JS, изображения, шрифты, иконки, карты источников, иногда JSON со справочниками. У статики есть два режима доставки: «свежая копия» (revalidation по ETag
/Last-Modified
) и «долгая жизнь» (годовой TTL) с immutable
и versioning в имени файла. Второй вариант быстрее, потому что браузер не делает условные запросы вовсе.
Простое правило: или короткий TTL и валидация через
etag/last-modified
, или очень длинный TTL сimmutable
и строгим versioning в имени файла. Смешивать подходы точечно можно, но осознанно.
Cache-Control и Expires: что важнее
Cache-Control
— современный и приоритетный заголовок управления кэшем. Директивы, которые чаще всего нужны:
public
— можно кэшировать в браузерах и промежуточных кэшах;private
— только браузер пользователя;max-age=N
— секунды жизни объекта;no-cache
— кэшировать можно, но перед использованием нужно валидировать (условный запрос);no-store
— запрещает хранение вообще (для чувствительных ответов, HTML админок);immutable
— браузер не будет пытаться валидировать объект в течениеmax-age
, если URL неизменен;s-maxage
— какmax-age
, но для прокси/CDN;stale-while-revalidate
иstale-if-error
— политика «можно отдавать устаревшее» во время обновления/ошибок (пригодно за прокси).
Expires
— устаревший, но ещё полезный заголовок с абсолютной датой. Если присутствуют оба, браузер ориентируется на Cache-Control
. Имеет смысл выставлять Expires
в дополнение к Cache-Control
ради старых клиентов и некоторых прокси.
ETag и Last-Modified: валидаторы и 304
ETag
— «отпечаток» версии ресурса. При повторном запросе браузер отправляет If-None-Match
; если совпало — сервер отвечает 304 Not Modified
без тела. Last-Modified
работает аналогично через If-Modified-Since
, но сравнивает дату.
Где они полезны:
- Для ресурсов без жёсткого versioning (например, аватарки пользователей, админские CSS/JS), когда правки редки и невелики.
- Для HTML и JSON, если вы не хотите долгих TTL и предпочитаете свежесть с минимальным трафиком.
Где лучше не полагаться на ETag
:
- Файлы с хешем в имени и
immutable
— валидатор не нужен и только создаёт лишние заголовки; - Кластеры, где генерация
ETag
зависит от inode/mtime — разные сервера могут выдавать разные значения, и кэш не попадёт. В таких случаях настройте составETag
или отключите его на статике.
Immutable и versioning: идеальная пара для статики
immutable
говорит браузеру «если URL не менялся, считать ресурс неизменяемым до истечения max-age
». Чтобы это было безопасно, используйте versioning в имени файла: app.abc123.css
, vendor.9f8e1.js
, logo.20250110.svg
. Хеш как часть имени — золотой стандарт.
Почему лучше не использовать query string (?v=123
) для статической версии:
- Некоторые прокси/кэши могут агрессивно игнорировать параметры либо иметь отдельные политики;
- Файловые CDN/облачные статики проще инвалидацировать по пути, чем по параметрам;
- Отладка и дедупликация артефактов в сборках проще, когда версия в имени файла.
Практика выкладки: HTML ссылается на актуальные *.hash.*
ассеты. Старые хеши не удаляйте сразу — оставьте на 7–30 дней, чтобы посетители с «долгими» кэшами не получали 404.
Стратегии по типам контента
HTML
Страницы лучше отдавать с Cache-Control: no-cache
или коротким max-age
(например, 60–300 секунд) плюс ETag
/Last-Modified
. Так вы сохраните свежесть и получите 304 при повторных заходах.
CSS/JS/шрифты/изображения
Для версионированных файлов выбираем долгий TTL (год) и immutable
. Для не-версионированных — умеренный TTL (дни/недели) плюс валидаторы.
API JSON и админка
Для чувствительных данных — no-store
, чтобы ничего не попадало в кэш, особенно на общих компьютерах. Для публичных справочников можно настроить no-cache
с валидаторами.
Nginx: два рабочих варианта
Вариант А: «современный» только с Cache-Control
# Включить/отключить валидаторы при необходимости
etag on;
last_modified on;
# Долгий TTL и immutable для версионированных ассетов (имя с хешем)
location ~* \.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$ {
# Если у вас строгий versioning в имени файла — можно отключить ETag
etag off;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# Умеренный TTL и валидация для не-версионированных
location ~* /assets/(?:legacy|uploads)/ {
etag on;
add_header Cache-Control "public, max-age=86400" always;
}
# HTML без кэша в прокси и с обязательной валидацией в браузере
location ~* \.(?:html)$ {
add_header Cache-Control "no-cache" always;
}
Вариант B: Cache-Control + Expires для совместимости
# Управляем и абсолютной датой Expires
location ~* \.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$ {
etag off;
expires 1y; # добавит Expires и Cache-Control: max-age, но мы переопределим CC ниже
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* \.(?:html)$ {
expires -1; # Expires в прошлом
add_header Cache-Control "no-cache" always;
}
Замечания:
add_header ... always;
гарантирует заголовки даже на 304/4xx/5xx;- Если у вас кластер, следите, чтобы часы синхронизировались (NTP), иначе
Last-Modified
может «скакать»; - Не забывайте про
Vary: Accept-Encoding
при раздаче gzip/br; в современных сборках Nginx это добавляется автоматически, но проверьте.
Если нужен полный контроль над конфигами, модулями и пакетами — разворачивайте всё на VDS. Для типовых сайтов с панелью подойдут тарифы на виртуальный хостинг.

Дополнительно про защитные заголовки (CSP, HSTS и др.) смотрите разбор в статье HTTP security headers для Nginx/Apache.
Apache (.htaccess): для shared/virtual hosting
На virtual hosting чаще всего доступен только .htaccess
. Включаем mod_expires
и mod_headers
:
<IfModule mod_expires.c>
ExpiresActive On
# По умолчанию ничего не кэшируем надолго
ExpiresDefault "access plus 0 seconds"
# Долгий TTL для типичных статических типов
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
# Долгий TTL + immutable для статики
Header set Cache-Control "public, max-age=31536000, immutable" "expr=%{CONTENT_TYPE} =~ m#^(text/css|application/(javascript|x-javascript)|image/|font/)#"
# HTML: без кэша, только валидировать
<FilesMatch "\.(?:html)$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
# ETag: избегаем inode для согласованности между серверами
FileETag MTime Size
# Можно убрать ETag у версионированных файлов (если имена с хешем)
<FilesMatch "\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$">
Header unset ETag
</FilesMatch>
Если у вас старая версия Apache без expr
в Header
, используйте FilesMatch
для назначения Cache-Control
по расширениям.
На виртуальном хостинге и в панелях часто есть предустановленные политики. Проверяйте фактические ответы через curl -I
и DevTools, чтобы убедиться, что панель не переписывает ваши заголовки.

Как распознать «версионированный» файл в конфиге
Иногда нужно различать файлы с хешем в имени и обычные. Пример для Nginx:
location ~* ^/assets/.+\.[0-9a-f]{8,}\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$ {
etag off;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* ^/assets/.+\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$ {
etag on;
add_header Cache-Control "public, max-age=604800" always; # 7 дней
}
Аналогично в Apache можно повесить разные блоки FilesMatch
на разные регулярные выражения.
Проверка и отладка
Минимальный чек-лист:
curl -I https://site.tld/assets/app.abc123.css
— убедитесь, что приходят нужныеCache-Control
/Expires
и валидаторы;- В DevTools браузера проверьте колонку «Size» — должен быть «(from disk cache)»/«(from memory cache)» на втором заходе;
- Измените версию ассета и убедитесь, что новая ссылка реально подхватывается без перезагрузки кэша;
- Проверьте
Vary
для сжатых ресурсов и наличиеContent-Encoding
; - На CDN проконтролируйте, что политики кэша не конфликтуют с вашими заголовками (например, не затирают
immutable
).
Типовые ошибки и как их исправить
- Долгий TTL на невариантном HTML. Итог: пользователи видят старые страницы. Решение:
no-cache
или короткийmax-age
плюс валидаторы. - Меняете файл на месте при
immutable
. Браузер не будет проверять обновление. Решение: всегда менять имя файла (versioning). - Несогласованные
ETag
на кластере. Разные сервера — разныеETag
. Решение: упроститьETag
(толькоMTime Size
) либо отключить для статики. - Ставите
no-store
на всё. Быстрое, но убивает производительность. Решение: применять точечно к чувствительным данным. - Неправильный
Vary
. ОтсутствиеVary: Accept-Encoding
ломает совместимость с прокси. Решение: включить авто-добавление или явно задать. - Ставите огромный
max-age
без versioning. Пользователи «залипают» на старых картинках/скриптах. Решение: дисциплина versioning.
Практика деплоя с учётом кэша
Грамотный пайплайн раздачи статики выглядит так:
- Собираете ассеты с контент-хешами в имени;
- Заливаете новые файлы рядом со старыми (старые временно оставляете);
- Обновляете HTML-шаблоны, чтобы они ссылались на новые имена;
- Выставляете заголовки: для новых —
public, max-age=31536000, immutable
; - Через 7–30 дней чистите старые ассеты (когда уверены, что активных кэшей не осталось).
Если что-то пошло не так, быстрый «откат» — вернуть старые HTML/шаблоны, указывающие на предыдущие версии файлов. Поэтому важно сохранять несколько прошлых поколений ассетов. Про миграции без простоя читайте в статье Переезд сайта без простоя.
Особенности для shared/virtual hosting
На виртуальном хостинге без root-доступа чаще всего доступен только .htaccess
(Apache) и панельные пресеты. Настроить cache-control
, expires
, etag
можно малыми силами, как показано выше. Если на фронте проксирует Nginx, убедитесь, что он не перезаписывает ваши заголовки. В ряде конфигураций панели предоставляют «переписчики» заголовков — проверяйте фактический ответ через curl -I
и DevTools.

Короткие «пресеты» для копипаста
Nginx: только versioned статика
location ~* ^/(assets|static)/.+\.[0-9a-f]{8,}\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf)$ {
etag off;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
Apache: аккуратное кэширование HTML
<FilesMatch "\.(?:html)$">
Header set Cache-Control "no-cache"
FileETag MTime Size
</FilesMatch>
Итоги
Для максимальной производительности статики выбирайте стратегию «долгий TTL + immutable + versioning в имени файла». Для HTML и не-версионированных ресурсов опирайтесь на etag/last-modified
с короткими TTL. В nginx
и apache
это настраивается несколькими директивами. Отлаживайте заголовки с помощью curl -I
и инструментов разработчика — это быстро окупается сокращением запросов, трафика и ускорением загрузки.