Обновлено:

Cache-Control

Кэш нужен не “для скорости вообще”, а для предсказуемого поведения сайта после обновлений.

У статического сайта обычно есть два разных типа файлов:

  1. HTML — должен быстро обновляться после деплоя.
  2. Хэшированные assets — CSS/JS с именами, которые меняются при изменении содержимого. Их можно кэшировать надолго.

Если эти правила перепутать, появятся странные проблемы: страница старая, стили новые; или наоборот — HTML обновился, а браузер держит старый JS.

Хорошая базовая схема

ЧтоПримерCache-Control
HTML/, /ru/docs/public, must-revalidate
Assets/assets/app.ABC123.jspublic, max-age=31536000, immutable
404/no-such-pagepublic, must-revalidate
Sitemap/sitemap-index.xmlpublic, must-revalidate
robots.txt/robots.txtpublic, must-revalidate

Почему HTML не надо кэшировать надолго

HTML — это точка входа. Именно он ссылается на актуальные CSS/JS-файлы.

Если поставить HTML:

Cache-Control: public, max-age=31536000, immutable

то браузер может долго не увидеть новую версию страницы после деплоя.

Для HTML лучше:

Cache-Control: public, must-revalidate

Это разрешает хранить файл, но требует перепроверять его актуальность.

Почему assets можно кэшировать надолго

Astro собирает CSS/JS в файлы с хэшами в имени. Пример:

/assets/cache-control.B1dARQ-M.css
/assets/hoisted.C4vOHhWK.js

Если содержимое меняется, имя файла тоже меняется. Поэтому старый asset можно безопасно держать в кэше долго.

Для assets подходит:

Cache-Control: public, max-age=31536000, immutable

immutable означает: “пока URL тот же, файл не изменится”.

Пример Nginx

Для assets:

location /assets/ {
    try_files $uri =404;
    access_log off;

    add_header Cache-Control "public, max-age=31536000, immutable" always;
    add_header X-Content-Type-Options nosniff always;
}

Для HTML и обычных путей:

location / {
    try_files $uri $uri/ =404;

    add_header Cache-Control "public, must-revalidate" always;
    add_header X-Content-Type-Options nosniff always;
}

Для 404:

error_page 404 /404.html;

location = /404.html {
    internal;
    add_header Cache-Control "public, must-revalidate" always;
}

Проверка HTML

curl -kI https://getsrv.app/

Ожидаемо:

HTTP/2 200
content-type: text/html
cache-control: public, must-revalidate

Проверка assets

ASSET="$(find /var/www/getsrv.app/assets -maxdepth 1 -type f | head -n 1 | sed 's#^/var/www/getsrv.app##')"
curl -kI "https://getsrv.app$ASSET"

Ожидаемо:

HTTP/2 200
cache-control: public, max-age=31536000, immutable

Проверка 404

curl -kI https://getsrv.app/no-such-page

Ожидаемо:

HTTP/2 404
cache-control: public, must-revalidate

Проверка sitemap

curl -kI https://getsrv.app/sitemap-index.xml

Ожидаемо:

HTTP/2 200
content-type: text/xml
cache-control: public, must-revalidate

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

Ошибка 1. expires и add_header Cache-Control вместе

В Nginx expires сам добавляет Cache-Control. Если рядом ещё вручную добавить Cache-Control, можно получить два заголовка.

Плохо:

expires 1h;
add_header Cache-Control "public, must-revalidate" always;

Лучше выбрать один способ. Для точного контроля проще использовать только add_header Cache-Control.

Ошибка 2. immutable для HTML

Плохо:

location / {
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

Так браузер может долго держать старый HTML.

Ошибка 3. Assets не в /assets/

Если сборщик складывает файлы в один путь, а Nginx кэширует другой путь, правила не сработают.

Проверьте Astro config:

build: {
  assets: 'assets'
}

И Nginx:

location /assets/ {
    ...
}

Ошибка 4. Старый HTML ссылается на удалённый JS

Если используется rsync --delete, старые assets удаляются. Это правильно, но при слишком долгом HTML-cache браузер может открыть старый HTML, который ссылается на уже удалённый JS.

Поэтому HTML — must-revalidate, assets — immutable.

Короткая проверка после деплоя

curl -kI https://getsrv.app/ | grep -i cache-control

ASSET="$(find /var/www/getsrv.app/assets -maxdepth 1 -type f | head -n 1 | sed 's#^/var/www/getsrv.app##')"
curl -kI "https://getsrv.app$ASSET" | grep -i cache-control

curl -kI https://getsrv.app/no-such-page | grep -i cache-control

Ожидаемый смысл:

HTML:  public, must-revalidate
Asset: public, max-age=31536000, immutable
404:   public, must-revalidate