1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 1 из 6

Как браузер решает, идти ли в сеть

Коротко

Самый быстрый запрос - тот, которого не было. Управляет этим сервер: какие заголовки nginx положит в ответ, так браузер и будет вести себя полгода.

  • Исходов три, а не два: не спрашивать (свежий по max-age), переспросить и получить 304, скачать заново.
  • 304 экономит трафик, но не время. Ускоряет только первый исход.
  • no-cache - это «кешируй, но каждый раз переспрашивай». Запрещает хранение no-store.
  • nginx сам отдаёт валидаторыВалидаторETag или дата последнего изменения. Клиент присылает его обратно, и если ничего не поменялось, сервер отвечает 304 без тела - трафик экономится, а круг по сети всё равно тратится., но не отдаёт Cache-Control, поэтому без явной настройки срок свежести браузер придумывает сам.
  • ETag собирается из времени и размера файла, а не из содержимого. Отсюда две практические беды: пересборка и второй сервер.

Если три исхода и устройство ETag знакомы - листай до «Теперь сам».

Сначала ответь сам

Файл сборки с хешем в имени - app.4f2c1b.js, содержимое которого по определению никогда не изменится. Конфиг обычный, ничего про кеш в нём не написано.

Как долго браузер будет считать этот файл свежим?

Инстинктивный ответ - «сервер что-нибудь разумное скажет». Настоящий: nginx не скажет ничего, и срок браузер придумает сам.

На что это похоже

Это разница между «знаю» и «уточню».

Ты держишь дома расписание электричек. Если на нём написано «действует до 1 декабря», до декабря ты в него просто смотришь - ноль усилий. Если срока нет, приходится каждый раз звонить и спрашивать, не поменялось ли. Тебе ответят «не поменялось» за пять секунд - трафика почти ноль, - но пять секунд ты потратил.

А если расписания нет вовсе, придётся ехать за новым.

Три исхода ровно те же, и разница между первым и вторым - не в объёме данных, а в том, был ли звонок.

Механизм: три исхода

1. свежий по max-age - запроса НЕТ вообще 0 мс, 0 байт 2. устарел, есть валидатор - условный запрос, ответ 304 поход в сеть плюс ~200 байт: трафик сэкономлен, время нет 3. нет в кеше или запрещено - обычный запрос, ответ 200 поход в сеть плюс вес файла
Разница между первой и второй строкой важнее, чем кажется: 304 стоит те же 100-300 мс на мобильной связи, что и полноценный ответ.

Свежесть задаёт Cache-Control: max-age=N - «этот ответ можно считать актуальным N секунд, не переспрашивая». Соседние директивы, которые встречаются каждый день:

Директива Что значит
public можно кешировать и промежуточным узлам (CDN - сеть серверов-копий поближе к пользователю, прокси)
private только браузеру конкретного пользователя
no-cache кешировать можно, но перед каждым использованием переспросить
no-store хранить нельзя вовсе
must-revalidate после истечения max-age отдавать протухшее нельзя (промежуточные кеши иногда так делают, если не достучались до сервера)
immutable не переспрашивать даже при обновлении страницы

Старый заголовок Expires задаёт ту же свежесть абсолютной датой. Он остался ради HTTP/1.0, и при конфликте Cache-Control главнее.

no-cache - это не «не кешировать»

Название обманывает уже двадцать лет. no-cache разрешает хранить копию и требует проверять её перед показом - в результате часто получается быстрый 304. Полный запрет хранения - это no-store, и он нужен для страниц с чужими персональными данными, а не для рядового HTML.

Правило

nginx сам отдаёт валидаторы (ETag и Last-Modified), но не отдаёт Cache-Control. Без явного max-age срок свежести выбирает браузер, и поведение получается разным в разных браузерах.

Разбор: что реально в ответе

Проверено на nginx 1.31.5, конфиг - голый root, никаких директив про кеш.

$  команда
curl -sI localhost/f.txt
↳  выводcurl -sI localhost/f.txt
HTTP/1.1 200 OK
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Content-Type: text/plain
Content-Length: 300
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: "6a995e03-12c"
Accept-Ranges: bytes

Ни одного Cache-Control. Это и есть ответ на вопрос из начала урока: срок свежести никто не назвал, и браузер вычислит его сам. Спецификация HTTP как типичное значение советует десятую часть возраста файла по Last-Modified: файлу, выложенному десять дней назад, браузер поверит примерно на сутки, а выложенному вчера - на пару часов. Файл с хешем в имени, который можно было бы кешировать на год, живёт по чужой эвристике. Не поломка, а настройка по умолчанию.

Валидаторы при этом работают как надо. Когда срок вышел, браузер сам присылает If-None-Match со значением ETag из своей копии; сервер сравнивает и, если совпало, отвечает 304 без тела:

$  команда
curl -sI -H 'If-None-Match: "6a995e03-12c"' localhost/f.txt
↳  выводcurl -sI -H 'If-None-Match: "6a995e03-12c"' localhost/f.txt
HTTP/1.1 304 Not Modified
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: "6a995e03-12c"

То есть без Cache-Control сайт, как только придуманный браузером срок истёк, живёт во втором исходе: трафик экономится, время - нет. Для страницы с двадцатью ассетами это двадцать походов в сеть на каждое открытие.

Разбор: где наивное правило подводит

Наивное правило: «ETagETagКороткая строка, по которой клиент может спросить «у меня версия такая, она ещё годится?». nginx собирает её из времени изменения и размера файла, а не из содержимого. - это отпечаток содержимого, как хеш файла».

Не содержимого. nginx собирает его из времени модификации и размера в шестнадцатеричном виде. Проверим на файле ровно в 300 байт:

$  команда
stat -c 'размер=%s mtime=%Y' f.txt; printf '%x-%x\n' $(stat -c '%Y %s' f.txt); curl -sI localhost/f.txt | grep ETag
↳  выводstat -c 'размер=%s mtime=%Y' f.txt; printf '%x-%x\n' $(stat -c '%Y %s' f.txt); curl -sI localhost/f.txt | grep ETag
размер=300 mtime=1788435971
6a995e03-12c
ETag: "6a995e03-12c"

stat -c печатает размер и время изменения файла в секундах от 1970 года, printf '%x' переводит оба числа в шестнадцатеричный вид.

Ровно две склеенные части. Из этого следуют две практические беды.

Пересборка ломает кеш, даже если содержимое не изменилось. Копируем тот же файл: cp f.txt f2.txt ставит копии текущее время.

↳  выводcurl -sI localhost/f2.txt | grep -E 'ETag|Last-Modified'
Last-Modified: Thu, 10 Sep 2026 19:52:58 GMT
ETag: "6aa30a9a-12c"

Содержимое байт в байт то же, ETag другой - изменилось время. Выложил сборку заново (скопировал файлы в каталог сайта), и все клиенты качают всё заново. Лечится тем, что выкладка сохраняет время файлов: rsync -a, а не cp. rsync - программа копирования, флаг -a сохраняет у файлов время и права.

На двух серверах ETag разные. Файлы разложены в разное время, значит один и тот же app.css за балансировщиком (он распределяет запросы между серверами) отдаёт разные валидаторы, и браузер перекачивает его при каждом переключении сервера. Лечение одно - одинаковое время файлов на всех серверах: выкладка через rsync -a или явное время при сборке. etag off; не поможет: Last-Modified - тоже время файла и разъедется так же.

Что ломается без этого

  • Каждое открытие страницы - десятки запросов с кодом 304. Нет Cache-Control, и браузер перепроверяет файлы, как только истёк придуманный им самим срок.
  • После выкладки все клиенты качают неизменившиеся файлы. Выкладка не сохранила время файлов.
  • За балансировщиком кеш работает вдвое хуже. Разные ETag на разных серверах.
  • no-store поставили туда, где хватило бы no-cache. Страница перестала кешироваться вовсе, хотя достаточно было проверки.

Зачем это в работе

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

Проверяется это за минуту на живом сайте:

$  команда
curl -sI https://сайт/assets/app.4f2c1b.js | grep -i cache-control

Пусто - значит все ассеты сайта живут по эвристике браузера, а затем во втором исходе, и открытие страницы стоит пользователю пачки round-trip - походов в сеть «запрос туда, ответ обратно». -i у grep - поиск без учёта регистра, у curl тот же флаг значит другое. На мобильной связи это заметно глазами: страница «думает» до того, как начать рисоваться.

Следующий урок - о том, как заменить умолчание осмысленной политикой, и там же разбирается, почему на разные типы файлов срок ставят разный.

Теперь сам

1. Чем 304 отличается от «файл взят из кеша» с точки зрения пользователя на мобильной связи?

Временем. 304 - это полноценный поход в сеть: соединение, запрос, ответ. Тела нет, поэтому трафик почти нулевой, но задержка та же, что у обычного ответа. Пользователь видит разницу, а счётчик трафика - нет.

2. Сайт отдаёт Cache-Control: no-cache на все страницы. Кешируются ли они?

Да, копия сохраняется. Но перед каждым показом браузер спрашивает сервер, и при неизменном файле получает 304. Это осмысленный выбор для HTML: пользователь всегда видит актуальную страницу, а трафик экономится.

3. Почему после выкладки через cp -r пользователи перекачивают всё, хотя изменился один файл?

cp ставит файлам текущее время, значит у всех новый ETag и новый Last-Modified. Для браузера это другие файлы. rsync -a сохраняет время и меняет валидаторы только у тех, что действительно изменились.

Главное

Браузер выбирает из трёх исходов, и ускоряет только первый - когда запроса не было вовсе. nginx сам отдаёт ETag и Last-Modified, но не отдаёт Cache-Control, поэтому без явной настройки срок свежести придумывает браузер, а дальше сайт живёт на 304. ETag собирается из времени и размера файла, а не из содержимого.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.