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

Immutable и cache-busting: правильные Cache-Control‑заголовки для CDN и браузеров

В этой инструкции разберёмся, как настроить долговечное кэширование статических файлов через CDN и Nginx: что значит директива immutable, как правильно делать cache-busting, какие заголовки Cache-Control ставить и как не наступить на типичные грабли при продакшен‑деплоях.
Immutable и cache-busting: правильные Cache-Control‑заголовки для CDN и браузеров

Если упростить, то «идеальный» фронтенд в продакшене складывается из двух вещей: неизменяемых статических ресурсов с очень долгим TTL и быстрой, предсказуемой доставки через CDN. На практике это означает связку из cache-busting (переменовываем файлы при каждом изменении) и заголовка immutable (говорим браузеру и прокси: «этот URL никогда не меняется»). Плюс корректные Cache-Control и прочие headers на уровне Nginx. Ниже — подробный, но прагматичный гид для админов и девопсов, как всё это собрать без ловушек.

Это легко запускается на origin‑сервере под Nginx: удобно держать его на VDS, а для HTTP/2/3 и CDN сразу оформляйте надёжные SSL-сертификаты.

Зачем immutable и cache-busting

Цель — минимальная латентность и стабильность кэшей. Когда статический файл (CSS, JS, шрифты, изображения) отдаётся с очень длинным max-age или s-maxage, CDN и браузер могут хранить его вечно (или год), не обращаясь к источнику. Но это безопасно только в одном случае: если URL ресурса уникален для конкретного содержимого. Тогда при любом изменении контента вы меняете и URL — это и есть cache-busting.

Директива immutable усиливает гарантию: она подсказывает браузеру, что ресурс по этому адресу не изменится совсем, поэтому нельзя тратить трафик на «пере‑проверки» при перезагрузке страницы или навигации назад/вперёд.

Ключевая идея: уникальный URL для каждого билда, плюс «долгий» Cache-Control и immutable. Тогда кэширование становится детерминированным, а деплой — безопасным и без принудительных очисток CDN.

Модель кэшей: где что хранится

В цепочке запроса обычно участвуют минимум три слоя: браузер пользователя (private cache), CDN/промежуточные прокси (shared cache) и origin (ваш Nginx). Управляем ими через заголовки:

  • Cache-Control: директивы public/private, max-age, s-maxage, immutable, no-cache, no-store, must-revalidate, stale-while-revalidate, stale-if-error.
  • ETag и Last-Modified — основа условных запросов (If-None-Match, If-Modified-Since), когда ресурс нельзя сделать «вечным».
  • Vary — различение кэша по заголовкам (например, Accept-Encoding, Origin, Accept).

Идеал для неизменяемых ресурсов — не полагаться на ETag/Last-Modified, а давать «глубокий» TTL и immutable. Для изменяемых — короткий TTL и валидаторы.

Диаграмма слоёв кэширования: браузер, CDN и origin

Что такое immutable на практике

Директива immutable в Cache-Control говорит браузеру: «не проверяй обновления при явной перезагрузке, не делай conditional requests для этого URL». Это критично ускоряет повторные заходы и офлайн‑навигацию. Но применять её можно только если URL никогда не «переиспользуется» для нового контента.

Поэтому правило №1: используйте cache-busting. Основные подходы:

  1. Хеш в имени файла: app.9f12ab.css, vendor.3c9e.js. Самый надёжный вариант: и браузеры, и CDN воспринимают URL как новый путь.
  2. Версионирование директорий: /assets/v123/app.css. Тоже надёжно, меняется путь.
  3. Query‑параметр: /app.css?v=123. Работает, но зависит от настройки CDN (должен учитывать всю строку запроса в ключе кэша). Если CDN игнорирует query‑string, то это опасно.

Отсюда правило №2: если используете query‑версии, убедитесь, что ваш CDN не настроен на «ignore query string». Иначе вы рискуете отдавать старый контент под новым параметром.

Политики кэширования по типам контента

Рассмотрим набор разумных политик:

  • HTML (страницы, SPA index.html): Cache-Control: no-cache (или max-age=0, must-revalidate). Браузер может хранить копию, но всегда валидирует. Для CDN можно задать s-maxage небольшим (например, 60) плюс stale-while-revalidate/stale-if-error, чтобы сгладить пики.
  • API JSON: короткий max-age или no-cache, обязательно ETag. CDN — по нагрузке: иногда s-maxage=30..300 и stale-while-revalidate, если ответы детерминированы.
  • Статика с cache-busting (CSS/JS/шрифты/изображения/видео/иконки): Cache-Control: public, max-age=31536000, immutable. Для CDN можно добавить s-maxage (если он у вас отличается от max-age), но часто достаточно одного.
  • Без cache-busting (например, медиа, которые могут перезаписываться тем же именем): ставьте небольшой max-age (например, 300..3600) и включайте валидаторы.
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Nginx: минимально удобная конфигурация

Ниже пример, где мы автоматически отличаем «захешированные» имена файлов и даём им «вечный» TTL с immutable. Для остальной статики — умеренный TTL, для HTML — валидация.

# В http{}
map $uri $is_hashed {
    default 0;
    ~-[0-9a-f]{8,}\.(?:css|js|mjs|map|png|jpg|jpeg|webp|avif|gif|svg|ico|woff2?|ttf|eot)$ 1;
}

map $is_hashed $cc_static {
    1 "public, max-age=31536000, immutable";
    0 "public, max-age=300";
}

server {
    listen 80;
    server_name example.org;

    # HTML: всегда валидируем (браузер), CDN можно настроить отдельно
    location ~* \.html$ {
        add_header Cache-Control "no-cache" always;
        try_files $uri =404;
    }

    # Статика: hashed получает вечный TTL, остальное — умеренный
    location ~* \.(?:css|js|mjs|map|png|jpg|jpeg|webp|avif|gif|svg|ico|woff2?|ttf|eot)$ {
        add_header Cache-Control $cc_static always;
        # по желанию: add_header Vary "Accept-Encoding" always;
        try_files $uri =404;
    }

    # По умолчанию
    location / {
        add_header Cache-Control "no-cache" always;
        try_files $uri $uri/ =404;
    }
}
  • map распознаёт паттерн «тире + 8+ hex в имени + ожидаемое расширение». Настройте список расширений под себя.
  • immutable выдаём только «гарантированно неизменным» URL.
  • Для HTML используется no-cache: копия может храниться, но при запросе происходит валидация.

Добавим CDN‑настройки и устаревание

Если у вас есть CDN и вы хотите более агрессивно использовать общий кэш, можно разделить политику для браузера и для CDN через s-maxage, а также включить «устаревание» на просадках источника:

map $is_hashed $cc_static {
    1 "public, max-age=31536000, s-maxage=31536000, immutable, stale-while-revalidate=86400, stale-if-error=86400";
    0 "public, max-age=300, s-maxage=600, stale-while-revalidate=600, stale-if-error=600";
}

Не все CDN учитывают stale-while-revalidate/stale-if-error одинаково, но даже если они игнорируются на периметре, браузерам эти директивы полезны.

Где пригодятся ETag и Last-Modified

На «вечной» статике с immutable валидаторы излишни — запросы валидации не происходят. Но для ресурсов без cache-busting они нужны. Пара нюансов:

  • В Nginx etag для статических файлов обычно включён, однако при on‑the‑fly компрессии (gzip) слабые/конфликтующие теги могут отключаться. Проверьте фактический ответ.
  • Last-Modified полезен как запасной валидатор; его удобно выставляет сам Nginx для файлов с диска.

Для API‑ответов ETag можно формировать приложением (хеш полезной нагрузки) и обрабатывать If-None-Match, отдавая 304 при совпадении.

Фрагменты конфигурации Nginx и проверка заголовков через curl

Cache-busting: три подхода и их последствия

Сравним стратегии на практике:

  • Хеш в имени файла. Плюсы: лучше всего работает с любыми кэшами и CDN, легко отличить «вечные» ресурсы на уровне Nginx. Минусы: нужно, чтобы сборщик/билдер прокладывал новые пути в шаблоны (Webpack/Rollup/Vite, bundlers бэкенда, collectstatic и пр.).
  • Версионные директории. Плюсы: удобно для отделения релизов (/assets/vNNN/…), позволяет держать N релизов параллельно. Минусы: требуется аккуратный деплой и зачистка старых директорий.
  • Query‑параметр. Плюсы: проще встроить в CMS/шаблоны. Минусы: зависимость от настроек CDN; некоторые ускорители агрегируют кэш по пути, игнорируя query — риск коллизий.

Если вы не уверены в политике CDN, используйте хеш в имени или версионные директории. Тогда immutable абсолютно безопасен. Дополнительно про хранение версий артефактов и кэширование на объектных стораджах разобрано в материале Версионирование в S3 и CDN: хранение и кэш.

Чего делать нельзя

  • Ставить immutable на HTML, JSON, динамические страницы и API — пользователи могут «залипнуть» на старой версии.
  • Выдавать «вечные» заголовки ресурсам без уникального URL — получите нескончаемые отладочные танцы и принудительные инвалидации.
  • Полагаться только на ETag для тяжёлой статики: это добавляет RTT на условные запросы и снижает выигрыш от CDN.
  • Даже с cache-busting удалять старые файлы сразу после релиза — браузеры и CDN ещё долго будут на них ссылаться с кешированных HTML; держите несколько релизов доступными.

CDN: кто главный — заголовки или панель

Большинство CDN по умолчанию уважают заголовки Cache-Control от origin, но в панели часто включены override‑правила. Проверьте:

  • Как формируется ключ кэша: учитывает ли query‑string, cookies, Vary и Accept-Encoding.
  • Не переписывает ли CDN max-age/s-maxage или не игнорирует ли immutable.
  • Включены ли механики «serve stale» при ошибках источника и как они сочетаются с вашими заголовками.

Хорошая практика — хранить максимум политики в ответах origin, а в панели держать минимальные исключения (например, принудительные правила для отдельных путей).

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Интеграция в CI/CD

Правильный деплой выглядит так:

  1. Сборщик создаёт артефакты с контент‑хешами в именах.
  2. Публикуете новый набор в новую директорию/путь. Старый остаётся доступным.
  3. Обновляете шаблоны/манифесты, чтобы HTML ссылался на новые URL.
  4. Раскатываете релиз. В этот момент у пользователей в кэшах — «вечные» файлы старого релиза, но HTML быстро переведёт их на новые.
  5. Через безопасный интервал удаляете устаревшие артефакты.

Если работаете с CMS, где автоматическая подмена путей сложна, используйте манифесты (asset-manifest.json) или функции‑помощники, подставляющие актуальные версии.

Проверка и отладка

Быстрые команды для ручной проверки заголовков и кэширующего поведения:

# Статика с хешем: ожидаем public, max-age=31536000, immutable
curl -I https://example.org/assets/app.9f12ab.css

# HTML: ожидаем no-cache (или max-age=0, must-revalidate)
curl -I https://example.org/index.html

# Повторный запрос к «вечной» статике должен НЕ слать If-None-Match
curl -I https://example.org/assets/app.9f12ab.css

# Проверка CDN-варьирования по кодировке
curl -I -H "Accept-Encoding: gzip" https://example.org/assets/app.9f12ab.css
curl -I -H "Accept-Encoding: br" https://example.org/assets/app.9f12ab.css

В браузере смотрите DevTools → Network: статус (from disk cache), (from memory cache) и наличие/отсутствие conditional запросов. Если при Ctrl+R для «вечной» статики браузер снова валидирует ресурс — проверьте, действительно ли есть immutable, и нет ли противоречащих директив.

Частые грабли

  • Дублирующийся Cache-Control из разных add_header — проверьте add_header ... always и порядок вложенных location.
  • Смешение Expires и Cache-Control с разными значениями — современные клиенты слушают Cache-Control, но лучше убрать противоречия.
  • immutable на ресурсах без cache-busting — пользователи «залипают» на старом контенте, CDN отдает устаревшее неделями.
  • Wrong Vary: забыли Vary: Accept-Encoding при on‑the‑fly компрессии — кэш смешивает gzip/br/identity варианты.
  • Очистка CDN вместо правильного именования: инвалидации — костыль. Лучше менять URL и оставлять старые файлы доступными на время.

Расширенные приёмы

  • Preload и приоритеты загрузки: если используете Link: <...>; rel=preload; as=script, следите, чтобы URL совпадал с «вечным» и не дублировал загрузку.
  • Шрифты: применяйте immutable к .woff2 с хешами; добавляйте font-display в CSS для UX.
  • Source maps: они тоже обычно хешируются и могут уезжать с «вечным» TTL; держите их доступными столько же, сколько поддерживаете релиз.
  • SRI: для критичных ресурсов совместите «вечное» кэширование с подписями Subresource Integrity — детали в статье про SRI и кэширование фронтенд-ресурсов.

Шаблонный Nginx для продакшена

Соберём чуть более полный фрагмент с типовыми расширениями, Vary, и аккуратным поведением по умолчанию.

map $uri $is_hashed {
    default 0;
    ~-[0-9a-f]{8,}\.(?:css|js|mjs|map|png|jpg|jpeg|webp|avif|gif|svg|ico|woff2?|ttf|eot|mp4|webm|ogg)$ 1;
}

map $is_hashed $cache_control_static {
    1 "public, max-age=31536000, s-maxage=31536000, immutable, stale-while-revalidate=86400, stale-if-error=86400";
    0 "public, max-age=600, s-maxage=1200, stale-while-revalidate=600, stale-if-error=600";
}

server {
    listen 80;
    server_name example.org;

    # HTML и пр. динамика: валидируем каждый раз
    location ~* \.(?:html|json)$ {
        add_header Cache-Control "no-cache" always;
        try_files $uri =404;
    }

    # Вся статика
    location ~* \.(?:css|js|mjs|map|png|jpg|jpeg|webp|avif|gif|svg|ico|woff2?|ttf|eot|mp4|webm|ogg)$ {
        add_header Cache-Control $cache_control_static always;
        add_header Vary "Accept-Encoding" always;
        try_files $uri =404;
    }

    # Остальное
    location / {
        add_header Cache-Control "no-cache" always;
        try_files $uri $uri/ =404;
    }
}

Этот шаблон хорош как стартовая точка. Под свои типы и пути его легко расширять.

FAQ: коротко о важном

  • Нужно ли immutable при query‑версии? Да, если ваш CDN учитывает query в ключе. В противном случае используйте хеш в имени.
  • Нужны ли ETag/Last-Modified при «вечной» статике? Нет, не критично. Для остальных ресурсов — да.
  • Сколько хранить старые релизы? Минимум столько, сколько длится TTL HTML у кэшей + запас. Часто 7–30 дней достаточно.
  • Что ставить на HTML SPA? Cache-Control: no-cache (или max-age=0, must-revalidate), можно s-maxage для CDN.

Итог

Надёжная схема кэширования для фронта выглядит так: внедрить cache-busting с изменением пути (хеши в именах или версионные директории), в Nginx настроить «вечные» заголовки Cache-Control с immutable только для таких URL, а для HTML и динамики — валидацию через no-cache и, при необходимости, короткий s-maxage для CDN. Добавьте тесты через curl -I и DevTools, держите несколько релизов параллельно — и кэш начнёт работать на вас: меньше запросов к origin, быстрее первая отрисовка, стабильнее пиковые нагрузки.

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

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

Nginx proxy_cache_path: разбор keys_zone, max_size, inactive и тонкая настройка OpenAI Статья написана AI (GPT 5)

Nginx proxy_cache_path: разбор keys_zone, max_size, inactive и тонкая настройка

Если вы кэшируете ответы через Nginx, директива proxy_cache_path определяет каталог на диске, глубину levels, объём keys_zone и пр ...
Apache mod_md и ACME: автоматизация SSL без и с cron OpenAI Статья написана AI (GPT 5)

Apache mod_md и ACME: автоматизация SSL без и с cron

Покажу, как включить модуль mod_md в Apache и настроить автоматизацию ACME с Let’s Encrypt без внешних скриптов. Разберём рабочие ...
HAProxy HTTP Cache: практическое руководство для реверс‑прокси OpenAI Статья написана AI (GPT 5)

HAProxy HTTP Cache: практическое руководство для реверс‑прокси

Разбираем встроенный HTTP cache в HAProxy: когда он уместен, как писать правила и учитывать headers, выбирать TTL, нормализовать з ...