Кэширование в вебе: Http cache-control, etag и service worker без подводных камней

Надёжное кэширование в вебе строится на трёх уровнях: 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 уместны

Кэширование в вебе: HTTP Cache-Control, ETag, Service Worker и подводные камни - иллюстрация
  • 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 осознанно.

Пошаговая инструкция: безопасный старт

  1. Определите, что кэшировать, а что запретить

    Разделите ресурсы на: immutable ассеты (JS/CSS с хэшем), HTML shell, публичные API, приватные API. Для приватных API сразу выберите network-only или no-store на сервере.

    • Immutable: cache-first
    • HTML: network-first с запасным офлайн-шаблоном
    • API: по умолчанию network-first или network-only
  2. Добавьте регистрацию Service Worker с контролем окружений

    Регистрируйте SW только там, где вы готовы его поддерживать (например, прод и стейдж), и логируйте версию. Это упрощает откат, если обновление пошло не так.

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js', { scope: '/' });
    }
  3. Реализуйте установку и предкэш только стабильных ассетов

    Предкэшируйте минимальный набор: 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))
      );
    });
  4. Добавьте обработчик 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))
        );
      }
    });
  5. Настройте очистку старых кэшей и контролируемое обновление

    При активации удаляйте устаревшие кеш-неймы. Версионируйте 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, где один "старый" бандл может сломать весь интерфейс.

Чек-лист проверки после релиза

Кэширование в вебе: HTTP Cache-Control, ETag, Service Worker и подводные камни - иллюстрация
  • Ассеты (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.

  1. Chrome DevTools (Network, Application)

    Уместно для быстрой диагностики: откуда взялся ресурс (memory/disk), какие заголовки пришли, какая версия Service Worker активна, что лежит в Cache Storage.

  2. curl для заголовков и условных запросов

    Уместно для воспроизводимых проверок в CI/чек-листах релиза. Хорошо ловит ошибки в Cache-Control, ETag, Last-Modified, Vary.

    curl -I https://example.com/
    curl -I https://example.com/assets/app.3f2c1a.js
  3. Логи origin и CDN

    Уместно, когда "в браузере всё нормально", но пользователи видят другое. Ищите HIT/MISS, различия ключа кэша, редкие 304 и неожиданные 200.

  4. Тестовый стенд с имитацией релиза

    Уместно для сложных SPA: прогоняйте сценарии обновления (открытая вкладка + новый деплой), проверяйте отсутствие разъезда версий и корректную очистку старых кэшей.

Короткие ответы на типичные затруднения

Почему после деплоя у части пользователей остаётся старая версия?

Кэширование в вебе: HTTP Cache-Control, ETag, Service Worker и подводные камни - иллюстрация

Чаще всего 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) и убедитесь, что обновление версии проходит без ручной очистки.

Прокрутить вверх