Версионированные ресурсы проходят через долгий кеш, а HTML регулярно проверяет обновление
Создание сайтов

Кеширование статических файлов в Nginx без старой версии сайта

Настраиваем Cache-Control для HTML и версионированных CSS, JavaScript и изображений, проверяем заголовки и избегаем зависшего старого релиза.

Содержание

После публикации новой версии посетитель может получить старый HTML, который ссылается на уже удалённый JavaScript. Обычно виноват не сам браузер, а одинаковая cache-политика для файлов с разным способом обновления.

HTML сохраняет прежний URL и должен регулярно проверяться у сервера. Файл вроде app.a1b2c3.js получает новое имя вместе с содержимым, поэтому старую версию можно хранить долго. Настроим эти две группы отдельно и проверим не текст конфигурации, а настоящие HTTP-заголовки.

Разделите ресурсы по способу обновления

Не выбирайте срок только по расширению. Важен контракт URL:

Тип Меняется ли URL с содержимым Практичная политика
HTML страницы обычно нет no-cache
CSS/JS с content hash да долгий max-age, immutable
изображение с хешем да долгий max-age, immutable
robots.txt нет короткий срок или no-cache
favicon с постоянным именем нет умеренный срок или no-cache
пользовательский ответ зависит от данных отдельная политика, часто private

Если сборщик создаёт app.XYZ.js, новая версия получает другой URL. Старый файл можно хранить долго: новый HTML уже ссылается на новое имя. Если вы заменяете /images/hero.webp по тому же пути, годовой immutable заставит часть клиентов видеть старую картинку.

Поймите no-cache и no-store

no-cache не запрещает хранение. Он требует revalidation перед повторным использованием. При наличии ETag или Last-Modified клиент отправляет условный запрос, а сервер может ответить 304 Not Modified без повторной передачи тела.

no-store требует не сохранять ответ. Это уместно для особенно чувствительных данных, но лишает преимуществ кеша и браузерной истории. Для обычного публичного HTML статического сайта чаще нужен no-cache, а не набор из всех запрещающих директив сразу.

Сначала посмотрите текущие заголовки

Запросите отдельно HTML и один ресурс, ссылка на который действительно находится в странице. Ключ --head получает только заголовки, не загружая тело ответа:

curl --fail --silent --show-error --head https://example.org/
curl --fail --silent --show-error --head https://example.org/assets/app.a1b2c3.js

Второй URL замените фактическим ресурсом из HTML. Смотрите:

  • Cache-Control;
  • Expires, если он используется;
  • ETag и Last-Modified;
  • Age и служебные заголовки CDN;
  • конечный код после редиректа;
  • Content-Type.

Не проверяйте только localhost, если перед сайтом есть CDN. Итоговую политику может изменить промежуточный слой.

Настройте HTML отдельно

Для HTML со стабильным URL используйте no-cache: браузер сможет сохранить документ, но перед повторным использованием должен подтвердить его актуальность. Блок располагается внутри нужного server:

location / {
    try_files $uri $uri/ =404;
    add_header Cache-Control "no-cache" always;
}

Это простая основа, но она добавит заголовок ко всем ответам данного location, включая некоторые assets, если более точный location их не перехватит. Поэтому дальше нужен отдельный блок для хешированных ресурсов.

Параметр always добавляет заголовок и к дополнительным кодам ответа. Проверьте, нужна ли одинаковая политика странице ошибки. Не ставьте один общий Cache-Control на весь server без анализа наследования.

Дайте долгий срок только хешированным assets

Если сборщик помещает файлы с хешем содержимого в /assets/, выделите этот каталог точным префиксом. Следующая политика разрешает публичному кешу хранить такие файлы один год:

location ^~ /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri =404;
}

add_header задаёт полную политику одним значением. Не добавляйте рядом expires без необходимости: эта директива тоже формирует Cache-Control с max-age, и итог получит два заголовка. Проверьте фактический ответ, потому что конфигурация на разных уровнях может переопределять наследование.

Год — распространённый срок для действительно версионированного ресурса, но контракт важнее числа. Файл по этому URL нельзя заменять другим содержимым до истечения freshness. Старые assets сохраняйте хотя бы вместе с релизами, пока старый HTML ещё может на них ссылаться.

Обработайте постоянные изображения отдельно

Для изображений без хеша в имени выберите более короткий срок, чтобы замена по тому же URL дошла до пользователей. Значение в примере составляет сутки и требует осознанной настройки под частоту обновления:

location ~* \.(?:png|jpg|jpeg|webp|avif|svg|ico)$ {
    add_header Cache-Control "public, max-age=86400";
    try_files $uri =404;
}

Это пример, а не обязательное значение. Если изображение меняется по прежнему URL, срок выбирают с учётом допустимой задержки обновления. Более надёжный вариант — менять URL вместе с содержимым.

Регулярное выражение может перехватить изображение из хешированного /assets/. Префиксный location с ^~ сохраняет для этого каталога отдельную политику, но итоговый выбор всё равно нужно проверить командой nginx -T и реальным запросом.

Не забывайте про 404 и удалённые assets

Неверная конфигурация может вернуть HTML-страницу ошибки для URL .js с кодом 200 и долгим кешем. Всегда используйте try_files $uri =404 для assets и проверяйте тип содержимого.

При атомарном deploy старый HTML может оставаться в браузере, поисковом кеше или открытой вкладке. Если cleanup немедленно удалит все assets предыдущего релиза, такая страница сломается. Сохраняйте несколько предыдущих релизов либо публикуйте assets в общем content-addressed каталоге.

Проверьте синтаксис и примените

Перед reload проверьте всё дерево конфигурации. Если тест успешен, перечитайте настройки и убедитесь, что служба осталась активной:

sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager

После reload повторите запросы к HTML, существующему ресурсу и отсутствующему файлу. Последний адрес должен вернуть настоящий 404:

curl --fail --silent --show-error --head https://example.org/
curl --fail --silent --show-error --head https://example.org/assets/app.a1b2c3.js
curl --silent --show-error --head https://example.org/assets/missing.js

Ожидайте revalidation для HTML, долгую свежесть только у хешированного asset и настоящий 404 для отсутствующего файла.

Проверьте условный запрос

Скопируйте значение ETag из ответа HTML и отправьте его обратно в условном заголовке. Так клиент спрашивает, изменился ли документ с этой версией:

curl --silent --show-error --head \
  --header 'If-None-Match: "actual-etag"' \
  https://example.org/

Подставьте фактическое значение вместе с нужными кавычками. При неизменном файле Nginx может вернуть 304. Это подтверждает, что no-cache не обязательно заставляет передавать весь HTML снова.

Учтите CDN и service worker

CDN различает browser cache и shared cache, может учитывать s-maxage и собственные правила. Не включайте «cache everything» до проверки персонализированных страниц, cookies и способов очистки. Для статического публичного HTML shared cache допустим только при понятной политике свежести.

Service worker создаёт ещё один уровень. Если он реализует cache-first для HTML без обновления, исправление Nginx не решит проблему. Проверьте код worker, имя cache и процедуру активации новой версии.

Как откатить неудачную политику

Верните предыдущий конфигурационный файл, выполните nginx -t и reload. Но новый заголовок не способен мгновенно удалить уже сохранённый fresh-ответ у всех клиентов. Поэтому опасный долгий immutable для постоянного URL исправляют новым URL ресурса или ожиданием срока; очистка CDN влияет только на управляемый промежуточный кеш.

Частые ошибки

  • Годовой кеш для HTML.
  • immutable у файла, который заменяется по тому же пути.
  • Удаление assets старого релиза сразу после deploy.
  • Один regex-location случайно переопределяет другой.
  • no-store применяется ко всему сайту без причины.
  • Проверяется конфигурация, но не внешний ответ.
  • CDN или service worker остаётся вне диагностики.

Проверяйте кеш-заголовки теми же внешними запросами, что используются в smoke-тестах. При атомарной публикации сохраняйте assets предыдущих версий дольше HTML, чтобы открытая вкладка не потеряла CSS или JavaScript сразу после релиза.

Первоисточники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Какая политика подходит HTML со стабильным URL?
02Какой ресурс подходит для долгого immutable-кеша?
03Чем проверить фактическую политику production?

Разбираем коротко

Частые вопросы

Почему HTML нельзя кешировать так же долго, как CSS с хешем?

URL HTML обычно остаётся прежним, а внутри него появляются ссылки на новые ресурсы. Долгий fresh-cache не обращается к серверу и продолжает показывать старую версию до истечения срока.

Означает ли Cache-Control no-cache полный запрет хранения?

Нет. Ответ можно сохранить, но перед повторным использованием кеш должен проверить его у сервера. Для полного запрета хранения предназначен no-store, который не стоит применять без причины.

Когда безопасно использовать immutable?

Когда URL ресурса меняется вместе с содержимым, например имя содержит content hash, и файл никогда не заменяется другим содержимым по тому же адресу в пределах срока кеша.