Надёжное кэширование в вебе строится на трёх уровнях: HTTP-заголовки (Cache-Control для правил хранения), валидаторы (ETag/Last-Modified для условных запросов) и Service Worker (для офлайн и тонких стратегий). Ключ к безопасности - разделять типы ресурсов, заранее продумать инвалидизацию при деплое и проверять поведение через DevTools и curl.
Краткие выводы и рабочие рекомендации
- Для статических ассетов используйте долгий TTL + версионирование в имени файла; для HTML - короткий TTL или
no-cacheс валидацией. - Настраивайте
Cache-Controlотдельно для HTML, API-ответов и статических ресурсов; это основа оптимизация скорости сайта кеширование без сюрпризов. - ETag/Last-Modified применяйте для условных GET, но не полагайтесь на них для приватных данных и персонализации.
- Service Worker включайте только с планом отката: неправильная стратегия легко "закрепляет" баги у пользователей.
- Инвалидизация - это не "сбросить кэш", а гарантия доставки новой версии: хэш-имена, манифест, корректные заголовки.
- Тестируйте кэш как систему: браузер + CDN + прокси + сервер; фиксируйте результаты в виде проверяемых команд.
Как работает HTTP Cache-Control: директивы и их приоритет
Кому подходит: всем публичным сайтам и приложениям, особенно при наличии CDN и статических ассетов. Это базовая настройка кеширования http cache-control, без которой остальные техники дают нестабильный эффект.
Когда лучше не делать агрессивно: для HTML со встроенной персонализацией, приватных API-ответов, страниц админки, а также любых ответов, где ошибка кэширования может раскрыть данные или сломать бизнес-логику.
Приоритет и смысл директив (практический минимум)
- public / private - можно ли хранить в shared-кэше (CDN/прокси) или только в браузере.
- max-age - время свежести; пока не истекло, кэш может отдавать ответ без обращения к серверу.
- s-maxage - аналог
max-age, но для shared-кэшей (CDN), часто важнееmax-ageименно там. - no-cache - хранить можно, но перед использованием нужно провалидировать у сервера (условный запрос).
- no-store - не хранить нигде (актуально для чувствительных данных).
- must-revalidate - после истечения свежести кэш обязан провалидировать, не должен "подсовывать" устаревшее.
- immutable - подсказка браузеру: ресурс не изменится в пределах TTL (безопасно только при версионировании URL).
Типовые политики по классам ресурсов
- Статические ассеты (JS/CSS/шрифты, с хэшем в имени):
Cache-Control: public, max-age=..., immutable. - HTML (SPA shell, страницы): часто
Cache-Control: no-cacheили небольшойmax-age; цель - быстро получить новую версию разметки. - API (особенно персонализированное): по умолчанию осторожно:
Cache-Control: private, no-cacheилиno-storeдля чувствительных ответов.
Сравнение подходов: Cache-Control, ETag/Last-Modified и Service Worker
| Подход | Назначение | Влияние на CDN | Риск |
|---|---|---|---|
| Cache-Control (max-age, s-maxage, no-cache, no-store) | Правила хранения и переиспользования ответа | Определяет, будет ли CDN хранить и как долго (особенно через s-maxage) |
Высокий при неверной классификации контента: утечка приватного, раздача устаревшего HTML |
| ETag / Last-Modified | Условные запросы (304 Not Modified) для экономии трафика | CDN может реже тянуть тело, но поведение зависит от конфигурации и ключа кэша | Средний: коллизии/нестабильные ETag, неверная валидация при разных представлениях ресурса |
| Service Worker | Клиентский контроль: офлайн, стратегии cache-first/network-first и т. п. | CDN остаётся полезным, но часть решений переезжает в клиент; ошибки сложнее диагностировать | Высокий: "закрепление" багов, несогласованность версий, кеширование приватного, сложный откат |
ETag и Last-Modified: когда использовать и как избегать коллизий
Если вам важно понять, как работает etag в http, думайте о нём как о "версии представления ресурса": клиент шлёт If-None-Match, сервер отвечает 304 без тела, если версия совпала. Last-Modified работает аналогично через If-Modified-Since, но обычно менее точен.
Когда ETag/Last-Modified уместны

- HTML/JSON, где нельзя ставить большой
max-age, но можно экономить на повторной загрузке. - Документы и данные, меняющиеся нерегулярно.
- API-ответы для списка/каталога при корректном разделении по пользователю (или для публичных данных).
Что понадобится (доступы и инструменты)
- Доступ к конфигурации веб-сервера/приложения, чтобы задавать/прокидывать
ETag,Last-Modified,Cache-Control,Vary. - Доступ к логам или трассировке (хотя бы на стейдже), чтобы видеть 200 vs 304 и заголовки.
- Chrome DevTools (Network) и возможность запускать
curlиз терминала.
Как снижать риск коллизий и "ложной свежести"
- Стабильность ETag: генерируйте ETag из контента/версии представления, а не из локальных метаданных, которые меняются от инстанса к инстансу.
- Учитывайте представление: если ответ зависит от
Accept-Encoding, локали или типа пользователя, задавайте корректныйVaryи убедитесь, что валидатор соответствует именно этому варианту ответа. - Слабые и сильные ETag: слабый ETag (
W/...) допустим, когда важна семантическая эквивалентность, но это усложняет ожидания клиентов и прокси.
Быстрая проверка условного запроса
curl -I https://example.com/app.js
# Допустим, сервер вернул ETag: abc123
curl -I https://example.com/app.js -H "If-None-Match: abc123"
Ожидаемо: второй запрос возвращает 304 Not Modified и минимальный набор заголовков. Если вместо 304 вы всегда видите 200 - валидаторы не работают или постоянно меняются.
На практике "кеширование в браузере cache-control etag" нужно проектировать вместе: Cache-Control задаёт правила, а ETag/Last-Modified дают безопасную валидацию, когда ресурс нельзя надолго "замораживать".
Service Worker: стратегии кеширования для офлайн и производительности
Подход service worker кеширование ресурсов полезен для SPA/PWA, где важно мгновенное повторное открытие, офлайн-режим и контроль над тем, что и когда обновляется. Делайте это только при понимании жизненного цикла SW и стратегии обновления.
Риски и ограничения, которые стоит принять до внедрения
- Риск закрепления багов: неправильная стратегия может продолжать отдавать старый JS даже после фикса на сервере.
- Сложная диагностика: ошибка может быть только у части пользователей из-за разных версий SW и кэша.
- Опасность для приватных данных: нельзя бездумно кэшировать персонализированные ответы API.
- Непредсказуемое обновление: новый SW активируется не мгновенно; нужно управлять
skipWaiting/clientsClaimосознанно.
Пошаговая инструкция: безопасный старт
-
Определите, что кэшировать, а что запретить
Разделите ресурсы на: immutable ассеты (JS/CSS с хэшем), HTML shell, публичные API, приватные API. Для приватных API сразу выберите
network-onlyилиno-storeна сервере.- Immutable: cache-first
- HTML: network-first с запасным офлайн-шаблоном
- API: по умолчанию network-first или network-only
-
Добавьте регистрацию Service Worker с контролем окружений
Регистрируйте SW только там, где вы готовы его поддерживать (например, прод и стейдж), и логируйте версию. Это упрощает откат, если обновление пошло не так.
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/' }); } -
Реализуйте установку и предкэш только стабильных ассетов
Предкэшируйте минимальный набор: CSS/JS с хэшем, иконки, офлайн-страницу. Не кладите в предкэш HTML, который часто меняется без версионирования URL.
// sw.js const CACHE_NAME = 'app-static-v1'; const PRECACHE_URLS = [ '/offline.html', '/assets/app.3f2c1a.js', '/assets/app.3f2c1a.css' ]; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) ); }); -
Добавьте обработчик fetch с двумя стратегиями
Для ассетов - cache-first, для навигации - network-first с офлайн-фолбэком. Так вы ускоряете повторные загрузки, но не "замораживаете" HTML навсегда.
self.addEventListener('fetch', (event) => { const req = event.request; // Навигация (HTML) if (req.mode === 'navigate') { event.respondWith( fetch(req).catch(() => caches.match('/offline.html')) ); return; } // Статика (пример по пути) if (new URL(req.url).pathname.startsWith('/assets/')) { event.respondWith( caches.match(req).then((cached) => cached || fetch(req)) ); } }); -
Настройте очистку старых кэшей и контролируемое обновление
При активации удаляйте устаревшие кеш-неймы. Версионируйте
CACHE_NAMEвместе с релизом, чтобы пользователи гарантированно получили новую статику.self.addEventListener('activate', (event) => { const keep = [CACHE_NAME]; event.waitUntil( caches.keys().then((keys) => Promise.all(keys.map((k) => (keep.includes(k) ? null : caches.delete(k)))) ) ); });
Инвалидизация кеша при деплое: безопасные подходы и схемы версионирования
Инвалидизация должна быть предсказуемой: новые ассеты приходят по новым URL, HTML быстро узнаёт о новой версии, а кэш не смешивает старое и новое. Это критично для SPA, где один "старый" бандл может сломать весь интерфейс.
Чек-лист проверки после релиза

- Ассеты (JS/CSS) имеют уникальные имена (хэш/версия) и отдаются с долгим
Cache-Controlтолько если URL действительно меняется при изменениях. - HTML отдаётся с
no-cache(или коротким TTL) и корректно валидируется, чтобы пользователь быстро увидел новую разметку. - Проверен сценарий "старый HTML + новый JS" и "новый HTML + старый JS": не должно быть несовместимости API/контрактов.
- Для API с персонализацией нет кэширования в shared-кэше: проверьте
private/no-store, и при необходимостиVary: Authorization, Cookie. - При использовании CDN проверены ключ кэша и поведение
s-maxage; нет кэширования по умолчанию там, где вы этого не планировали. - Service Worker (если есть) обновляется: новая версия
sw.jsреально скачивается, старые кэши очищаются, офлайн-страница актуальна. - С DevTools подтверждено, что загрузка ассетов идёт из памяти/диска (когда ожидается), а HTML и критичные запросы не "залипают".
- Выполнена проверка через
curl -Iдля ключевых URL: заголовки соответствуют классу ресурса (HTML/ассет/API).
Подводные камни: безопасность, консистентность и ложные позитивы
- Кэширование приватного в CDN: отсутствие
private/no-storeи/или неверныйVaryможет привести к выдаче чужих данных. - Долгий TTL для HTML: приводит к "призракам" старой версии, особенно в SPA, где HTML задаёт загрузку бандлов.
- Несовместимые релизы: если фронт и бэк деплоятся отдельно, кэш легко фиксирует промежуточные состояния.
- Нестабильные ETag: если ETag меняется при каждом запросе (или зависит от инстанса), вы не получите 304 и будете думать, что "кэш не работает".
- Смешивание вариантов ответа: отсутствие корректного
Vary(например, поAccept-Encodingили авторизации) ломает кэш и может портить контент. - Service Worker кэширует всё подряд: слишком широкий match по URL перехватывает API и страницы, которые должны быть network-only.
- Сложный откат: без версионирования кэшей и понятного механизма обновления SW пользователи продолжают жить на старом коде.
- Ложные проверки: DevTools с включённым "Disable cache" или инкогнито может скрыть реальные проблемы, а корпоративные прокси - добавить свои.
Инструменты и методики для тестирования и отладки кеширования
Эти варианты дополняют друг друга; выбирайте по тому, где именно ломается цепочка браузер → CDN → origin.
-
Chrome DevTools (Network, Application)
Уместно для быстрой диагностики: откуда взялся ресурс (memory/disk), какие заголовки пришли, какая версия Service Worker активна, что лежит в Cache Storage.
-
curl для заголовков и условных запросов
Уместно для воспроизводимых проверок в CI/чек-листах релиза. Хорошо ловит ошибки в
Cache-Control,ETag,Last-Modified,Vary.curl -I https://example.com/ curl -I https://example.com/assets/app.3f2c1a.js -
Логи origin и CDN
Уместно, когда "в браузере всё нормально", но пользователи видят другое. Ищите
HIT/MISS, различия ключа кэша, редкие 304 и неожиданные 200. -
Тестовый стенд с имитацией релиза
Уместно для сложных SPA: прогоняйте сценарии обновления (открытая вкладка + новый деплой), проверяйте отсутствие разъезда версий и корректную очистку старых кэшей.
Короткие ответы на типичные затруднения
Почему после деплоя у части пользователей остаётся старая версия?

Чаще всего HTML или Service Worker кэшируются слишком агрессивно, либо ассеты не версионируются в URL. Проверьте заголовки HTML и обновление sw.js.
Что выбрать для HTML: max-age или no-cache?
Для HTML безопаснее no-cache (с валидацией), чтобы клиент быстро узнавал о новой версии. Долгий max-age для HTML применяйте только при строгой схеме обновления.
ETag обязателен, если уже есть Cache-Control?
Нет. ETag полезен, когда ресурс нельзя делать "свежим" надолго, но можно экономить на повторной передаче через 304.
Почему я не вижу 304, хотя ETag есть?
Либо клиент не отправляет If-None-Match, либо ETag меняется на каждый запрос, либо ответ нельзя кэшировать из-за политики. Проверьте запрос/ответ в DevTools или через curl.
Можно ли кэшировать API в Service Worker?
Можно, но осторожно: публичные данные - да, персонализированные - обычно нет. Начинайте с network-first и чётких правил исключения.
Что опаснее: CDN-кэш или Service Worker?
Service Worker опаснее в плане закрепления ошибочного поведения на клиенте. CDN чаще проще инвалидировать, но он критичен по безопасности при неверных private/public и Vary.
Как понять, что кэш реально ускоряет, а не просто маскирует проблемы?
Сравните "холодные" и "тёплые" загрузки, проверьте источники ресурсов (network vs cache) и убедитесь, что обновление версии проходит без ручной очистки.

