Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

Кэширование статики правильно: Cache-Control, ETag, immutable и версияция ассетов

Разбираем, как кэшировать статический контент без сюрпризов: что ставить в Cache-Control и Expires, когда включать ETag/Last-Modified, зачем immutable и как делать версионирование ассетов. Даём готовые пресеты для Nginx/Apache и разбор типовых ошибок.
Кэширование статики правильно: Cache-Control, ETag, immutable и версияция ассетов

Если вы хотите, чтобы сайт открывался «из памяти браузера» и не тратил лишние запросы, вам нужны корректные заголовки для статики и дисциплина при выкладке. В этой статье разберём, как согласовать 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 или отключите его на статике.

DevTools: проверка кэш-хитов и заголовков статики

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. Для типовых сайтов с панелью подойдут тарифы на виртуальный хостинг.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Дополнительно про защитные заголовки (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 по расширениям.

.htaccess: директивы Cache-Control и Expires для статики

На виртуальном хостинге и в панелях часто есть предустановленные политики. Проверяйте фактические ответы через curl -I и DevTools, чтобы убедиться, что панель не переписывает ваши заголовки.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Как распознать «версионированный» файл в конфиге

Иногда нужно различать файлы с хешем в имени и обычные. Пример для 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.

Практика деплоя с учётом кэша

Грамотный пайплайн раздачи статики выглядит так:

  1. Собираете ассеты с контент-хешами в имени;
  2. Заливаете новые файлы рядом со старыми (старые временно оставляете);
  3. Обновляете HTML-шаблоны, чтобы они ссылались на новые имена;
  4. Выставляете заголовки: для новых — public, max-age=31536000, immutable;
  5. Через 7–30 дней чистите старые ассеты (когда уверены, что активных кэшей не осталось).

Если что-то пошло не так, быстрый «откат» — вернуть старые HTML/шаблоны, указывающие на предыдущие версии файлов. Поэтому важно сохранять несколько прошлых поколений ассетов. Про миграции без простоя читайте в статье Переезд сайта без простоя.

Особенности для shared/virtual hosting

На виртуальном хостинге без root-доступа чаще всего доступен только .htaccess (Apache) и панельные пресеты. Настроить cache-control, expires, etag можно малыми силами, как показано выше. Если на фронте проксирует Nginx, убедитесь, что он не перезаписывает ваши заголовки. В ряде конфигураций панели предоставляют «переписчики» заголовков — проверяйте фактический ответ через curl -I и DevTools.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Короткие «пресеты» для копипаста

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 и инструментов разработчика — это быстро окупается сокращением запросов, трафика и ускорением загрузки.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...