# теория · шаг 1 из 6
Как браузер решает, идти ли в сеть
Коротко
Самый быстрый запрос - тот, которого не было. Управляет этим сервер: какие заголовки nginx положит в ответ, так браузер и будет вести себя полгода.
- Исходов три, а не два: не спрашивать (свежий по
max-age), переспросить и получить 304, скачать заново. - 304 экономит трафик, но не время. Ускоряет только первый исход.
no-cache- это «кешируй, но каждый раз переспрашивай». Запрещает хранениеno-store.- nginx сам отдаёт валидаторыВалидаторETag или дата последнего изменения. Клиент присылает его обратно, и если ничего не поменялось, сервер отвечает 304 без тела - трафик экономится, а круг по сети всё равно тратится., но не отдаёт
Cache-Control, поэтому без явной настройки срок свежести браузер придумывает сам. ETagсобирается из времени и размера файла, а не из содержимого. Отсюда две практические беды: пересборка и второй сервер.
Если три исхода и устройство ETag знакомы - листай до «Теперь сам».
Сначала ответь сам
Файл сборки с хешем в имени - app.4f2c1b.js, содержимое которого по
определению никогда не изменится. Конфиг обычный, ничего про кеш в нём не
написано.
Как долго браузер будет считать этот файл свежим?
Инстинктивный ответ - «сервер что-нибудь разумное скажет». Настоящий: nginx не скажет ничего, и срок браузер придумает сам.
На что это похоже
Это разница между «знаю» и «уточню».
Ты держишь дома расписание электричек. Если на нём написано «действует до 1 декабря», до декабря ты в него просто смотришь - ноль усилий. Если срока нет, приходится каждый раз звонить и спрашивать, не поменялось ли. Тебе ответят «не поменялось» за пять секунд - трафика почти ноль, - но пять секунд ты потратил.
А если расписания нет вовсе, придётся ехать за новым.
Три исхода ровно те же, и разница между первым и вторым - не в объёме данных, а в том, был ли звонок.
Механизм: три исхода
Свежесть задаёт 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
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
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
размер=300 mtime=1788435971
6a995e03-12c
ETag: "6a995e03-12c"
stat -c печатает размер и время изменения файла в секундах от 1970 года,
printf '%x' переводит оба числа в шестнадцатеричный вид.
Ровно две склеенные части. Из этого следуют две практические беды.
Пересборка ломает кеш, даже если содержимое не изменилось. Копируем тот же
файл: cp f.txt f2.txt ставит копии текущее время.
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
собирается из времени и размера файла, а не из содержимого.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий