Как браузер проверяет, изменился ли ресурс

Браузер отправляет условный запрос — это запрос с заголовками If-None-Match (для ETag) или If-Modified-Since (для Last-Modified). Сервер проверит по этим данным: ничего не изменилось? Возвращает 304 Not Modified без тела ответа. Если ресурс изменился — полноценный 200 OK со всем контентом.

Это экономит трафик радикально. Вот механика: при первой загрузке сервер отправляет заголовки ETag и Last-Modified. Браузер сохраняет их в памяти вместе с самим файлом. Когда max-age из Cache-Control истекает, браузер не слепо берёт файл с диска — отправляет на сервер эту информацию обратно. Сервер мгновенно сравнивает: совпадает ли ETag? Дата последнего изменения новее? Если нет — 304. Трафик — только заголовки, килобайты вместо мегабайт.

ETag и Last-Modified: разные логики

ETag — это уникальный идентификатор версии ресурса, обычно хеш содержимого. Last-Modified — дата последнего изменения файла в GMT. Две разные оси. Если содержимое CSS не изменилось, ETag останется одинаковым, даже если сервер пересчитает дату. Last-Modified привязана ко времени на диске.

ETag выигрывает в распределённых системах. На YouTube много серверов, одна версия видео доступна с разных хостов. ETag гарантирует, что это одна и та же версия контента независимо от того, какой сервер её обслуживает. Last-Modified может рассинхронизироваться между серверами — у одного время файла 12:00, у другого 12:05. Браузер запрашивает файл с сервера A, потом с сервера B, и видит по-разному.

AWS S3 использует ETag для управления версиями объектов. Когда загружаете файл в бакет, S3 возвращает его ETag. При повторной загрузке того же файла браузер может отправить If-None-Match с этим ETag — S3 мгновенно проверит совпадение.

Крупные сервисы обрабатывают огромное количество ответов 304. Когда пользователь перезагружает страницу, браузер отправляет If-None-Match с сохранённым ETag. Сервер за микросекунду сравнивает ETag в памяти и возвращает 304. Трафик практически нулевой.

Что видно в DevTools браузера

Откройте вкладку Network, перезагрузите страницу (не очищая кэш). Найдите CSS- или JS-файл. В столбце «Size» — не объём файла, а статус «(от кэша)» или «304». Кликните на файл, откройте вкладку Headers. В ответе сервера найдёте:

  • ETag: "5f3c2a1b" — это и есть уникальный идентификатор версии. В двойных кавычках.
  • Last-Modified: Mon, 30 Sep 2024 14:23:00 GMT — дата в GMT.
  • Cache-Control: public, max-age=31536000 — браузер кэширует на год и будет проверять через год.

Во вкладке Request Headers увидите обратные заголовки:

  • If-None-Match: "5f3c2a1b" — браузер отправляет сохранённый ETag на проверку.
  • If-Modified-Since: Mon, 30 Sep 2024 14:23:00 GMT — отправляет дату последнего изменения.

Как Cache-Control и заголовки работают вместе

Cache-Control public max-age=31536000 означает: браузер кэширует файл на год без проверок. Но когда этот год прошёл, браузер отправляет If-None-Match или If-Modified-Since. Cache-Control no-cache требует проверять сохранённый ответ перед повторным использованием. Cache-Control no-store запрещает сохранять ответ.

Для статических файлов (CSS, JS, изображений) используют:

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

Директива immutable сообщает браузеру, что файл не изменится в течение срока действия кеша. Это работает, когда вы используете хеш в имени файла: style.a1b2c3.css. Новая версия — новый хеш, новое имя файла, новый запрос.

Для HTML используют:

Cache-Control: no-cache, must-revalidate, private

Браузер кэширует HTML, но при каждой загрузке отправляет If-None-Match или If-Modified-Since. Сервер быстро проверит через ETag и Last-Modified, вернёт 304 или новую версию.

Сценарии на практике

Сценарий 1: CSS не изменился. Загрузили style.css с etag="abc123". Браузер сохранил. Через месяц пользователь перезагружает страницу. Если max-age не истёк, браузер возьмёт из памяти. Если истёк, отправит If-None-Match: «abc123». Сервер проверит: да, ETag совпадает. Вернёт 304 Not Modified. Браузер использует закэшированный CSS. Трафик — пара килобайт, не мегабайт.

Сценарий 2: CSS изменился, но имя файла не изменилось. Неправильный подход, кстати. Вы обновили стили, но браузер всё ещё запрашивает style.css (не style.новыйхеш.css). Сервер отправляет новый etag="def456". Браузер отправляет If-None-Match: «abc123», сервер отвечает: «нет, ETag отличается». Возвращает 200 OK с новым CSS. Работает, но медленнее.

Сценарий 3: Last-Modified вместо ETag. Браузер сохранил Last-Modified: Mon, 30 Sep 2024 14:23:00 GMT. При повторном запросе отправляет If-Modified-Since с этой датой. Сервер сравнивает: дата на сервере старше или равна? Если старше, вернёт 304. Если новее, вернёт 200 OK. Работает, но менее надёжно в распределённых системах.

Nginx и правильная настройка

Nginx может генерировать ETag автоматически по хешу содержимого и времени последнего изменения:

location ~* \.(css|js|jpg|png)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
  etag on;
}
location / {
  expires -1;
  add_header Cache-Control "no-cache, must-revalidate, private";
  etag on;
}

Для статики: год кэша и ETag вычисляется один раз. Для HTML: без кэша, но с валидацией через ETag.

Реальная проблема с версионированием

Разработчик иногда не генерирует ETag для API. Express-сервер, например, может не включить ETag по умолчанию. Или генерирует одинаковый ETag для разных версий файла — браузер подумает, что ничего не изменилось, и покажет старые данные. Другая проблема: Last-Modified может гулять между серверами. На балансировщике два сервера, на одном дата 14:00, на другом 14:05. Клиент запрашивает с одного сервера, потом с другого — и видит нестабильное поведение кэша.

If-None-Match и If-Modified-Since: как сервер проверяет

Браузер отправляет:

If-None-Match: "5f3c2a1b"
If-Modified-Since: Mon, 30 Sep 2024 14:23:00 GMT

Сервер сравнивает: есть ли в памяти версия с таким же ETag? Если да — 304 Not Modified. Если нет, но дата последнего изменения на сервере старше или равна If-Modified-Since — тоже 304. Если совпадают оба условия, 304. Если хотя бы одно не совпадает и ресурс обновился — 200 OK с новым контентом.

Почему это важно для разработчика

Правильная валидация через ETag и Last-Modified экономит трафик, ускоряет загрузку, снижает нагрузку на сервер. На высоконагруженных системах это значит разницу между нормальной работой и перегрузом. CDN использует эти заголовки для кэширования между собой. Браузер использует для локального кэша. API должны поддерживать If-None-Match, иначе пользователи будут скачивать одинаковые данные каждый раз.