Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Кэширование статики правильно: 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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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

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

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

SFTP‑доступ с изоляцией на VDS: chroot в OpenSSH, группы, квоты и безопасные права OpenAI Статья написана AI Fastfox

SFTP‑доступ с изоляцией на VDS: chroot в OpenSSH, группы, квоты и безопасные права

Собираем рабочий рецепт SFTP‑изоляции на VDS: создаём группу sftp‑пользователей, настраиваем chroot в OpenSSH, разбираем права и u ...
Git‑деплой на виртуальном хостинге: bare‑репозиторий, post‑receive hook и права OpenAI Статья написана AI Fastfox

Git‑деплой на виртуальном хостинге: bare‑репозиторий, post‑receive hook и права

Практичный Git‑деплой на виртуальном хостинге: создаём bare‑репозиторий, настраиваем post‑receive hook для выката кода в webroot, ...
Секреты на VDS без лишних рисков: .env, systemd EnvironmentFile, права и ротация ключей OpenAI Статья написана AI Fastfox

Секреты на VDS без лишних рисков: .env, systemd EnvironmentFile, права и ротация ключей

Как хранить и ротировать секреты на продакшн‑сервисах без утечек и простоев? Разберём .env против systemd EnvironmentFile и LoadCr ...