От браузера к прокси-серверу: три уровня кеширования
Итак, вы добавили заголовок Cache-Control с max-age в ответ приложения. Но это только начало истории. На самом деле кеширование работает одновременно на трёх разных уровнях, каждый из которых преследует свои цели и работает по-разному.
Уровень 1: браузер пользователя
Первый и ближайший к пользователю уровень — это кеш самого браузера. Когда браузер получает ответ сервера, он смотрит на HTTP-заголовки. Конкретно на заголовок Cache-Control и его директивы. Если там написано cache-control: public, max-age=31536000, браузер сохраняет содержимое на локальный диск и на целый год просто подгружает этот файл оттуда. Сервер уже не видит этого запроса вообще.
На практике это выглядит так. Скажем, CSS-файл страницы сайта помечен директивой cache-control public max-age 31536000. Первый пользователь открывает страницу — CSS загружается с сервера приложения. Тот же пользователь заходит снова через минуту — браузер берёт CSS из локального хранилища. Третий, четвёртый, сотый заход — все они используют ту же копию CSS, которая была загружена в первый раз.
Это резко снижает нагрузку на сервер. Вместо того чтобы отправить одну и ту же CSS тысячу раз в день, сервер отправляет её один раз.
Уровень 2: прокси-серверы и CDN
Между браузером пользователя и вашим сервером приложения находятся промежуточные узлы. Это может быть прокси-сервер интернет-провайдера, это может быть кеш-узел Cloudflare, Akamai или других провайдеров CDN. Каждый из них может хранить копию ответа от сервера.
Когда браузер запрашивает страницу сайта и встречает на пути прокси-сервер, этот прокси смотрит на те же HTTP-заголовки Cache-Control. Если директива разрешает (cache-control: public), прокси копирует ответ себе в память и на диск. Следующие сотни пользователей получают эту копию с прокси-сервера, а не с вашего сервера приложения.
Это критично для масштабирования. YouTube не может на каждый видеозапрос обращаться за metadata в базу данных. Вместо этого metadata кешируется на уровне CDN. Когда пользователь из Новосибирска запрашивает видео, он получает ответ от ближайшего edge-сервера CDN в том же регионе. Задержка сети падает, например, со 150 до 10 миллисекунд. Нагрузка на центральный сервер приложения падает в сотни раз.
Но есть нюанс. Если вы используете директиву cache-control: private, то прокси-сервер не имеет права кешировать это содержимое. Private используется для персональных данных — истории заказов, профиля пользователя, персональных данных аккаунта. Вы не хотите, чтобы персональные данные одного пользователя хранились на промежуточном узле и были видны другому пользователю.
Уровень 3: серверное кеширование приложения
Третий уровень — это кеш на самом приложении. Это не браузер, не прокси-сервер, а сам сервер приложения. Здесь живут результаты дорогих операций: запросы к базе данных, вызовы внешних API, вычисленные значения. Часто используют Redis или Memcached, чтобы хранить эти данные.
Например, запрос в базу для получения информации о товаре занимает 100 миллисекунд. Вы кешируете результат на 5 минут. За эти 5 минут сервер приложения не совершает ни одного запроса в базу — просто возвращает результат из Redis. Это снижает нагрузку на сервер базы данных и ускоряет ответ приложения в 10 раз.
Stripe делает точно так же. Информация о rate limits (сколько запросов вы можете совершить в секунду) кешируется на сервере. Вместо того чтобы запрашивать эту информацию из базы на каждый API-запрос, сервер берёт её из памяти и отвечает за микросекунды.
HTTP-заголовки: как работает управление кешем
Главный инструмент — это заголовок Cache-Control. Это не чёрный ящик. Это конкретные директивы, которые браузер, прокси-серверы и сам сервер приложения должны соблюдать.
Директивы Cache-Control
Cache-Control: public говорит, что ресурс можно кешировать везде — и в браузере, и в прокси, и на edge-сервере CDN. Обычно это статические файлы: CSS, JavaScript, изображения. Их содержимое не привязано к конкретному пользователю.
Cache-Control: private указывает кешировать только в браузере пользователя, но не в промежуточных прокси-серверах. Это персональные данные, история, профиль.
Cache-Control: max-age=3600 задаёт время жизни кеша в секундах. 3600 — это один час. После этого браузер считает, что кеш устаревший и нужно проверить у сервера, не изменилось ли содержимое. Max-age 31536000 (один год) ставят на файлы вроде app.v1.23.min.js. Если выпустить новую версию, она получит новое имя (app.v1.24.min.js), новый URL, и браузер автоматически загрузит её.
Директива no-cache не означает «не кешировать вообще». Это означает «кешируй, но всегда проверяй у сервера перед использованием». No-store означает фактически «не кешируй совсем» — ресурс больше никогда не сохраняется.
Заголовок Expires и условные запросы
Раньше, до Cache-Control, использовали заголовок Expires. Он указывал конкретный момент времени (например, Wed, 01 Jan 2025 00:00:00 GMT), после которого кеш больше не актуален. Сейчас Expires редко используют — Cache-Control max-age удобнее.
Но есть механизм, который работает вместе с обоими. Когда время жизни кеша истекает, браузер может отправить условный запрос. Это запрос, в котором браузер говорит серверу: «У меня есть копия ресурса — может быть, она ещё свежая?»
Для этого браузер использует заголовок If-None-Match с ETag (это вроде отпечатка пальца ресурса). Или If-Modified-Since с Last-Modified (дата последнего обновления). Сервер быстро проверяет — изменилось ли содержимое. Если нет, отвечает кодом ответа 304 Not Modified. Браузер получает пустой ответ, понимает, что кеш свежий, и использует старую версию.
Это кажется странным — зачем отправлять запрос, если результат пуст? Потому что условный запрос без содержимого — это килобайты, а полный ответ с содержимым HTML — это мегабайты. Даже если браузер отправляет условный запрос, экономия трафика огромная.
Как работает механика в реальном сценарии
Представьте интернет-магазин на вашем сайте. HTML-страница товара имеет cache-control: public, max-age=3600 (кешируется на час). CSS-файлы имеют cache-control: public, max-age=31536000 (год, с версией в имени). Изображения товаров имеют cache-control: public, max-age=86400 (сутки).
Первый пользователь открывает страницу товара. Браузер пользователя отправляет запрос серверу приложения. Сервер отправляет полный HTML-ответ со всеми HTTP-заголовками. Браузер сохраняет этот HTML на час. Отдельно браузер запрашивает CSS — сервер отправляет ответ на год. Браузер кеширует ещё на год.
Тот же пользователь открывает страницу снова через 20 минут. Браузер проверяет кеш — та же страница и CSS уже там, ещё свежие (прошло только 20 минут). Браузер загружает из памяти локально, сетевых запросов не совершает. Страница загружается в разы быстрее.
Второй пользователь находится в том же городе, подключен через того же интернет-провайдера, у которого есть прокси-сервер. Его браузер требует страницу — промежуточный прокси-сервер перехватывает запрос. Может быть, эту страницу уже запрашивали другие пользователи? Прокси проверяет свой кеш — да, есть. Прокси отправляет ответ с этой копии, не трогая ваш сервер приложения.
Третий пользователь в Екатеринбурге, подключен к Cloudflare CDN. Его браузер требует страницу через Cloudflare. Cloudflare видит, что это публичный ресурс, кешируемый на час. Cloudflare сохраняет копию на edge-сервере в Екатеринбурге. Следующие тысячи пользователей в городе загружают страницу с этого edge-сервера, а не из центра.
Результат: один ответ от вашего сервера приложения, распространённый через браузер, прокси и CDN на десятки тысяч пользователей.
Стратегии работы с временем жизни кеша
На практике разработчики используют две основные стратегии.
Первая: кеширование с версионированием
Вы задаёте max-age на максимум (год, два года, бесконечность) и добавляете версию в имя файла. app.v1.js → app.v2.js. Когда версия изменится, URL изменится, браузер загрузит новый файл. Статические файлы CSS, JS, изображения часто кешируют именно так. Это надёжно: пока вы не обновите HTML и не добавите новый скрипт, старый кеш используется. Когда обновляете — браузер видит новый URL и уходит на сервер.
Вторая: кеширование с валидацией через ETag
Вы говорите браузеру кешировать ресурс бесконечно, но браузер перед каждым использованием отправляет условный запрос. Сервер быстро проверяет ETag или Last-Modified — изменилось ли содержимое? Если нет — отвечает 304 Not Modified. Если да — отправляет полный ответ 200 с новым содержимым и новым ETag.
Это гарантирует свежесть, но требует условного запроса. На слабой сети это может быть проблема.
На практике комбинируют. Статические файлы кешируют на год с версией в имени. HTML-страницы кешируют на несколько часов или совсем без кеширования (cache-control: no-cache), чтобы пользователь видел новую версию быстро. API-ответы кешируют в зависимости от того, как часто обновляются данные.
Примеры настройки в коде
Вот как это настраивается на разных языках.
Python + Flask:
@app.route('/assets/style.css')
def get_css():
return send_file('style.css',
add_etags=True,
max_age=31536000,
cache_control='public')
Node.js + Express:
app.get('/api/articles', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600');
res.json(articles);
});
PHP:
header('Cache-Control: public, max-age=3600');
echo json_encode($data);
Java + Spring:
@GetMapping("/static/**")
public ResponseEntity<?> serveStatic(HttpServletRequest request) {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
.body(content);
}
Это всё. Пять-десять строк, и система кеширования начинает работать. Браузер экономит трафик, сервер получает меньше запросов, пользователи видят страницы быстрее.
Но важно понимать: это рекомендации, не гарантии. Браузер может игнорировать Cache-Control, если памяти мало. Прокси-сервер может решить, что не кеширует определённый ресурс. CDN может вытеснить кеш по разным причинам. Поэтому кеширование — это инструмент оптимизации, который работает в 99% случаев, но в 1% случаев нужна резервная инструкция.
