Кеширование в браузере
Самый быстрый запрос — тот, которого не было. Кеш — единственная оптимизация, которая делает загрузку ресурса бесплатной, и настраивается она за один вечер.
HTTP-кеш браузера — это хранилище ответов сервера (в памяти и на диске), из которого браузер берёт файл повторно: либо вообще молча, либо спросив у сервера одним дешёвым вопросом «а он изменился?».
Зачем это нужно
Посмотрите на любую метрику из предыдущих разделов — LCP, TBT, вес бандла. Всё это мы оптимизируем, борясь за десятки килобайт и сотни миллисекунд. А кеш работает грубее и сильнее: он убирает запрос целиком. Ноль байтов по сети, ноль ожидания DNS, TCP и TLS, ноль нагрузки на ваш сервер.
Порядок величин такой. На мобильном 4G один сетевой круг (RTT) — это 50–150 мс. Чтобы получить первый байт файла со свежего соединения, нужно 3–4 таких круга: DNS, TCP-рукопожатие, TLS-рукопожатие, сам запрос. То есть даже пустой файл в 1 КБ «стоит» полсекунды. Файл из дискового кеша отдаётся за 1–5 мс, а из памяти — почти мгновенно. Разница не в процентах, а в порядках.
Практический эффект: правильно настроенный кеш превращает повторный визит на сайт в загрузку одного HTML-документа. Всё остальное — JS, CSS, шрифты, картинки, иконки — уже лежит у пользователя. Первый визит вы этим не ускорите, но второй, третий и все следующие (а это обычно большинство ваших сессий) станут почти мгновенными. И всё это — за счёт нескольких строк в конфиге, без единой строчки кода.
Cache-Control: заголовок, который решает всё
Кешированием управляет сервер, отправляя вместе с файлом заголовок Cache-Control. Браузер ему подчиняется. Выглядит это так:
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 121840
Cache-Control: public, max-age=31536000, immutable
ETag: "a3f19c-7b2e"Директив много, но на практике вам нужны шесть:
| Директива | Что означает |
max-age=N | Ответ считается свежим N секунд. Всё это время браузер берёт его из кеша вообще без запроса к серверу. |
no-cache | Кешировать можно, но перед каждым использованием надо переспросить сервер («не изменилось?»). Это не запрет кеша. |
no-store | Вот это — настоящий запрет: не писать ответ ни на диск, ни в память. Для страниц с приватными данными. |
public | Ответ можно кешировать не только браузеру, но и общим кешам — CDN, корпоративному прокси. |
private | Только личный кеш браузера. CDN такой ответ хранить не должен. |
immutable | Обещание: пока ответ свежий, он точно не изменится. Браузер не будет перепроверять его даже при нажатии «Обновить». |
Запомните главное различие, на котором спотыкаются почти все: no-cache и no-store — совершенно разные вещи. no-cache означает «кешируй, но валидируй». no-store — «не смей сохранять». Имена, к сожалению, выбраны неудачно.
ETag и условные запросы
Когда время жизни max-age истекло, браузер не скачивает файл заново вслепую. Он идёт на сервер с условным запросом: «дай мне файл, но только если он отличается от того, что у меня уже есть». Идентификатором версии служит ETag — короткая строка-отпечаток содержимого (обычно хеш), которую сервер прислал в прошлый раз. Есть и более старый механизм на дате — Last-Modified / If-Modified-Since, он менее точен (гранулярность в секунду) и работает так же по смыслу.
// Запрос браузера при повторном визите
GET /assets/logo.svg HTTP/1.1
If-None-Match: "a3f19c-7b2e"
// Ответ сервера, если файл не менялся
HTTP/1.1 304 Not Modified
ETag: "a3f19c-7b2e"
Cache-Control: public, max-age=600Ответ 304 Not Modified не содержит тела: сервер экономит трафик и говорит «бери свою копию». Это уже неплохо — вместо 40 КБ едет пара сотен байт заголовков. Но круг по сети всё равно совершается, а он и есть самое дорогое. Условный запрос — это компромисс, а не победа. Победа — когда браузер не спрашивает вообще.
Хеш в имени файла и вечный кеш
Как разрешить браузеру не спрашивать сервер год, но при этом уметь выкатить новую версию сегодня? Ответ придуман давно и работает безотказно: сделать имя файла зависимым от его содержимого. Любой современный сборщик (Vite, webpack, Rollup, esbuild, Next.js) умеет это из коробки:
dist/assets/index-4f2a9c1e.js
dist/assets/vendor-9b1c02af.js
dist/assets/style-7ac31140.css
dist/assets/Inter-var-c3d90b21.woff2Здесь 4f2a9c1e — хеш от содержимого файла. Поменялась хоть одна строчка кода — поменялся хеш, а значит, и URL. Новый URL браузер никогда не видел, старый кеш ему не помеха, он честно скачает новый файл. А старый файл по старому URL действительно неизменен — он не может измениться по определению. Значит, ему можно смело выдать max-age на год и immutable.
Отсюда рождается простое правило всего двух политик, которое покрывает 95% сайтов:
| Что | Cache-Control | Почему |
| Статика с хешем в имени (JS, CSS, шрифты, картинки сборки) | public, max-age=31536000, immutable | Содержимое по этому URL никогда не изменится. Кешируем навечно (год — максимум, который имеет смысл). |
HTML-документ (index.html, страницы SSR) | no-cache | Именно он содержит ссылки на новые хеши. Он обязан быть свежим, иначе деплой не доедет до пользователя. |
В nginx это буквально десять строк:
location /assets/ {
# Файлы с хешем в имени — вечный кеш, без перепроверок
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location = /index.html {
# HTML всегда валидируем: он раздаёт ссылки на новые хеши
add_header Cache-Control "no-cache";
}
location / {
try_files $uri /index.html;
}Проверить результат можно прямо из терминала, не открывая DevTools:
curl -sI https://example.com/assets/index-4f2a9c1e.js | grep -i cache-control
# cache-control: public, max-age=31536000, immutable
curl -sI https://example.com/ | grep -i cache-control
# cache-control: no-cacheКак это работает
Когда браузеру нужен ресурс, он проходит по цепочке сверху вниз и останавливается на первом успехе:
- Memory cache. Ресурс уже загружался в этой же вкладке — берётся из оперативной памяти за доли миллисекунды. Живёт до закрытия вкладки, заголовками не управляется.
- Disk cache, ответ свежий. С момента загрузки прошло меньше
max-ageсекунд — файл читается с диска, сеть не трогается вообще. В DevTools вы увидите статус200и размер(disk cache). - Disk cache, ответ протух.
max-ageистёк (или стоитno-cache) — браузер отправляет условный запрос сIf-None-Match. Если сервер ответил304, копия из кеша объявляется снова свежей и используется. Тело не качается. - Сеть. Кеша нет, или сервер ответил
200с новым телом — полноценная загрузка.
Директива immutable вмешивается в третий шаг: она говорит браузеру не отправлять условный запрос даже тогда, когда пользователь нажал «Обновить страницу» (F5). Без неё Chrome и Safari при перезагрузке ревалидируют всю статику, и вы получаете десяток бесполезных запросов с ответом 304 — по кругу сети каждый.
Отдельно стоит знать про stale-while-revalidate=N. Эта директива говорит: «после протухания ещё N секунд можешь отдавать старую копию мгновенно, а свежую подтянуть фоном». Идеально для данных, где допустима лёгкая несвежесть — аватарки, ленты, конфиги: Cache-Control: max-age=60, stale-while-revalidate=600.
Частые ошибки
- Длинный
max-ageна HTML. Классика, которая ломает деплой: пользователь неделю видит старую версию сайта, потому что егоindex.htmlзакеширован на 7 дней и ссылается на старые хеши. Откатить это нельзя — заголовок уже у него в кеше. HTML — толькоno-cache. - Путаница
no-cacheиno-store. Ставятno-cacheна страницу личного кабинета, думая, что запретили кеширование. Ответ при этом честно лежит на диске и достаётся оттуда после304. Для приватных данных нуженno-store. - Cache-busting через query-параметр вместо хеша в имени. Приём
/app.js?v=17работает в браузере, но некоторые прокси и старые CDN игнорируют query-строку при кешировании и отдают не тот файл. Хеш в имени файла надёжнее. - Забыли про
immutable. Кеш вроде настроен на год, но каждое обновление страницы всё равно даёт пачку запросов с304. На мобильной сети это лишние полсекунды на ровном месте. - ETag на кластере из нескольких серверов. Nginx и Apache по умолчанию генерируют ETag из inode и времени файла, а они на разных машинах разные. Один и тот же файл получает разные ETag — ревалидация никогда не срабатывает, каждый раз качается тело. Лечится хешем от содержимого или отключением ETag (
etag off), еслиLast-Modifiedхватает. - Отладка с включённым «Disable cache». В DevTools этот флажок стоит у большинства разработчиков постоянно, и они месяцами не видят, что кеш вообще не настроен. Проверяйте в обычном окне или через
curl -I.
Итоги
- Кеш — самая дешёвая оптимизация: он убирает запрос целиком, а не ускоряет его. Ноль байтов, ноль RTT, ноль нагрузки на сервер.
- Всей политикой управляет заголовок
Cache-Control.no-cache— «кешируй, но валидируй»,no-store— «не сохраняй». Это разные вещи. ETag+304 Not Modifiedэкономят трафик, но не экономят круг по сети. Это компромисс, а не цель.- Рабочая схема на два яруса: статика с хешем в имени —
public, max-age=31536000, immutable; HTML —no-cache. - Хеш в имени файла делает вечный кеш безопасным: новая версия — новый URL, старый кеш не мешает.
- Проверяйте настройку через
curl -I, а не глазами в DevTools с включённым «Disable cache».