Статика с длинным кешем в Nginx: immutable и версии
Длинный кеш ускоряет загрузку CSS, JavaScript, шрифтов и изображений, но легко превращается в причину «старой» версии сайта. Показываем, как связать Cache-Control: immutable с версионированием файлов и не закешировать HTML навсегда.

Если Nginx отдаёт CSS и JavaScript с кешем на год, браузер может не заметить новый релиз до истечения этого срока. Безопасная схема для Nginx 1.25.x состоит из двух частей: URL статического файла меняется при изменении содержимого, а прежний URL получает Cache-Control: public, max-age=31536000, immutable. В статье сравним способы версионирования, покажем конфигурацию и разберём, где длинный кеш ломает обновления.
Почему одного длинного кеша недостаточно
max-age=31536000 разрешает кешу считать ответ свежим 31 536 000 секунд, то есть до года. Пока этот срок не закончился, браузер обычно не обязан обращаться к серверу за новой копией файла. Если сайт продолжает ссылаться на /assets/app.js, после публикации новой сборки пользователь может получить старый код.
Кеш не знает, что файл на сервере изменился. Для него URL и правила свежести важнее содержимого. Поэтому исправление JavaScript в файле с прежним именем не решает проблему у тех, кто уже получил старый ответ.
Длинный кеш безопасен не потому, что браузер умеет угадывать обновления, а потому, что обновлённый файл получает новый URL.
Immutable1 сообщает кешу, что ресурс не изменится в течение срока свежести. Это уменьшает лишние проверки и особенно полезно для файлов с хешем в имени. Но immutable не заменяет max-age, не очищает кеш и не заставляет CDN немедленно удалить уже сохранённый ответ.
Три способа менять URL статического файла
Цель у всех вариантов одна: после изменения содержимого HTML должен ссылаться на новый адрес. Отличаются предсказуемость, работа с CDN и удобство отката.
| Способ | Пример | Плюсы | Цена и риск |
|---|---|---|---|
| Хеш содержимого в имени | app.8f3c1a.js | URL меняется только при изменении файла; хорошо работает с браузерами, CDN и прокси | Нужна сборка, которая переименовывает файлы и обновляет ссылки |
| Номер релиза в имени или каталоге | /assets/v42/app.js | Просто читать и откатывать релиз целиком | Файл может получить новую версию даже без изменения содержимого; старые каталоги нужно удалять по политике хранения |
| Параметр запроса | app.js?v=42 | Минимальные изменения в структуре файлов | Разные прокси и CDN по-разному учитывают query string; легко забыть изменить параметр |
Для production-сборки обычно выбирают хеш содержимого. Номер релиза удобен, если развёртывание уже организовано каталогами версий. Параметр запроса оставьте для простых проектов, где вы контролируете весь путь доставки и проверили поведение CDN.
Как настроить Nginx
Добавьте длинный кеш только для статики
В конфигурации ниже правило применяется к типичным CSS, JavaScript, шрифтам, изображениям и картам исходников. Версия синтаксиса рассчитана на Nginx 1.25.x; директива add_header относится к модулю HTTP headers.
location ~* \.(?:css|js|mjs|map|woff2?|ttf|otf|png|jpg|jpeg|gif|svg|webp|avif|ico)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
Расширьте список только теми форматами, которые действительно публикуются как неизменяемые версии. Не добавляйте в это правило HTML: браузер должен чаще проверять страницу, иначе она продолжит ссылаться на старые имена ресурсов.
Проверьте наследование директив
У add_header есть важная особенность Nginx: если на текущем уровне объявлен собственный add_header, настройки с более высокого уровня не наследуются автоматически. Из-за этого правило для безопасности или другого заголовка в конкретном location может скрыть ожидаемый Cache-Control.
Ещё один источник ошибки — заголовок Cache-Control, который уже добавляет приложение или прокси перед Nginx. Проверьте, что в ответе нет двух противоречивых значений: например, длинного кеша и no-cache.
Проверьте фактический ответ
Проверяйте не файл на диске, а публичный URL через тот же домен, которым пользуется браузер:
curl -I https://example.com/assets/app.8f3c1a.js
В ответе должен быть один согласованный заголовок Cache-Control с max-age=31536000 и immutable. Если перед сайтом стоит CDN, повторите проверку через его адрес и посмотрите, не добавляет ли он собственный Age, Via или другое правило кеширования.
Как связать хеши с HTML
Смысл хеширования не в красивом имени, а в автоматической связи между результатом сборки и HTML. Например, сборка создаёт app.8f3c1a.js и записывает это имя в шаблон. После изменения JavaScript появляется app.71bd20.js, а новая страница содержит уже его.
Старый файл не нужно заменять новым под тем же URL. Его можно оставить на сервере на время, пока CDN и браузеры используют предыдущую HTML-версию. Удаляйте старые ассеты только после периода, достаточного для отката и завершения публикации, иначе старая страница может начать получать ошибку 404.
Если проект использует манифест сборки, HTML или серверный шаблонизатор должен брать имя файла из этого манифеста, а не хранить его вручную. Ручная замена хеша в одном шаблоне часто оставляет старые ссылки в письмах, лендингах и кэшируемых фрагментах страниц.
Когда выбирать каждый вариант
- Хеш содержимого — основной выбор для webpack, Vite, Rollup и других сборок, где имена ресурсов уже формируются автоматически.
- Версия релиза — удобна для серверных приложений с атомарным переключением каталогов и быстрым откатом на предыдущий выпуск.
- Параметр запроса — допустим для небольшого сайта, если вы проверили, что CDN учитывает query string при выборе объекта кеша.
- Короткий кеш без immutable — компромисс для файлов, которые пока нельзя версионировать, например для внешнего виджета с фиксированным URL.
Не смешивайте длинный кеш с постоянным именем файла. Если URL изменить нельзя, уменьшайте срок свежести и заранее принимайте стоимость повторных запросов. «Принудительно обновить кеш» через очистку CDN можно, но это операционная процедура, а не замена версионированию.
Что ломается чаще всего
Новый JavaScript есть, но браузер запускает старый
Проверьте вкладку Network и фактический URL скрипта. Если в HTML осталось прежнее имя, проблема находится в сборке или шаблоне, а не в Nginx. Если имя новое, сравните ответ CDN и origin: промежуточный кеш мог сохранить HTML со старой ссылкой.
После релиза часть пользователей получает 404
Обычно старые файлы удалили раньше, чем перестали использовать старые страницы. Храните несколько последних наборов ассетов либо публикуйте файлы до переключения HTML и удаляйте их отдельным этапом.
Заголовок есть только на ответе 200
Без always Nginx добавляет заголовок только к ограниченному набору кодов ответа. Для обычной статики это не критично, но единое поведение для ошибок упрощает диагностику. При этом не стоит считать ошибочный ответ валидным кешируемым ресурсом: проверьте, какие правила для 404 и 5xx задаёт ваш CDN.
Кешируется HTML
Поищите более общее правило location или заголовок от приложения. Отделите обработку HTML от расширений статики и проверьте главную страницу командой curl -I. Для HTML обычно нужна отдельная политика, зависящая от способа публикации и авторизации.
Что делать дальше
Сначала выберите один способ версионирования и добавьте проверку: каждый URL из нового HTML должен отличаться после изменения содержимого. Затем измерьте долю попаданий в кеш на CDN и время загрузки критических CSS и шрифтов. После этого настройте хранение нескольких предыдущих наборов ассетов и протестируйте откат: старая HTML-версия не должна потерять свои JavaScript и CSS.
Инвалидация CDN и стратегия кеширования HTML — отдельные темы. Здесь ключевая граница простая: immutable можно ставить только на ресурс, чей URL меняется вместе с содержимым.
1 Immutable — директива Cache-Control, которая говорит совместимому кешу не выполнять повторную проверку свежего ресурса в течение заданного max-age.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: NGINX documentation: ngx_http_headers_module, MDN Web Docs: Cache-Control header
Частые вопросы
Можно ли поставить immutable на файл app.js с постоянным именем?
Что лучше: хеш в имени файла или параметр ?v=42?
Нужно ли удалять старые файлы после релиза?
Почему Cache-Control не появился в ответе Nginx?
Работает ли immutable в любом браузере и CDN?
Можно ли использовать max-age=31536000 для шрифтов и изображений?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.