Кеширование HTTP: от заголовков до Service Workers

Сегодня не медленный интернет — вы просто не закэшировали свой API

Ресурсоемкие запросы повторяются каждый раз, когда пользователь перезагружает страницу. Сервер крутит те же вычисления, база данных шлет идентичные наборы данных, трафик тратится на биты, которые уже путешествовали по сети. Современные веб-приложения потребляют больше данных, чем когда-либо, и умное кеширование стало критически важным навыком разработчика. Рассмотрим комплексный подход от серверных заголовков до продвинутых клиентских стратегий.

Серверное управление кешем

Cache-Control: микрооптимизации с макроэффектом

Простой заголовок Cache-Control — ваш первый инструмент оптимизации. Недостаточно просто добавить max-age, нужно стратегическое управление:

http
Cache-Control: public, max-age=3600, must-revalidate, stale-while-revalidate=86400

Разберем ключевые директивы:

  • public разрешает кеширование CDN и прокси
  • must-revalidate требует проверки актуальности после истечения max-age
  • stale-while-revalidate разрешает отдавать устаревшие данные на время фонового обновления (до 24 часов в примере)

Для динамических данных используйте private, запрещая прокси-кеширование:

http
Cache-Control: private, max-age=60

ETag и Last-Modified: валидация вместо перезагрузки

Когда двусторонняя валидация работает правильно, она экономит трафик и снижает нагрузку:

http
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

Клиент отправляет эти значения в последующих запросах:

http
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

Сервер возвращает 304 Not Modified для неизмененных ресурсов. Важные нюансы:

  • Для ETag сильные валидаторы (на основе контента) предпочтительнее слабых (на основе модификации)
  • Cloudflare и другие CDN по умолчанию игнорируют слабые ETag
  • Для динамически генерируемых контента вычисление ETag должно быть дешевле повторной генерации

На Node.js с Express имплементация выглядит так:

javascript
const crypto = require('crypto');

app.get('/data', (req, res) => {
  const data = fetchDataFromDB();
  const etag = crypto.createHash('sha1').update(JSON.stringify(data)).digest('hex');
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.set('ETag', etag);
  res.json(data);
});

Клиентские стратегии: за пределы браузерного кеша

Service Worker: кеширующий супергерой

Service Worker (SW) дает беспрецедентный контроль над сетевыми запросами. Рассмотрите стратегию Stale-While-Revalidate для критичных API:

javascript
// sw.js
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('api-cache').then(cache => {
      return cache.match(event.request).then(cachedResponse => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          // Обновляем кеш в фоновом режиме
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        
        // Отдаем кеш сразу, если есть, затем обновляем
        return cachedResponse || fetchPromise;
      });
    })
  );
});

Особенности реализации:

  • Обработка ошибок сети — критическая часть: если сеть недоступна, возвращаем кеш
  • Для POST-запросов нужно явное исключение — избегайте кеширования изменяющих операций
  • Используйте versioned caching для учёта изменений структуры ответов

Инвалидация клиентского кеша

Самая сложная проблема клиентского кеширования — когда обновлять. Стратегии:

  1. API versioning: URL включает версию (/api/v1/data)
  2. Content fingerprinting: путь включает хэш содержимого (/assets/main.abc123.js)
  3. Server-driven invalidation: при обновлении данных сервер отсылает update event через WebSockets
  4. Timeout fallback: SW обновляет данные в фоне при старте приложения

Реализация SW с приоритетом сети, но с кешированием при ошибках:

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // Для GET запросов обновляем кеш
        if (event.request.method === 'GET') {
          const clone = response.clone();
          caches.open('runtime-cache')
            .then(cache => cache.put(event.request, clone));
        }
        return response;
      })
      .catch(() => {
        // При ошибке сети ищем в кеше
        return caches.match(event.request);
      })
  );
});

Когда что использовать: рекомендации архитектуры

Статические ассеты

  • Cache-Control: public, max-age=31536000, immutable
  • Контроль версий через fingerprint в имени файла
  • Для устаревших браузеров — query-string versioning (e.g., ?v=1.2.3)

Динамические данные

  • Короткий max-age (60-300 сек) + ETag/Last-Modified
  • stale-while-revalidate для плавной инвалидации
  • Для времени-sensitive данных: no-cache с коротким stale-while-revalidate

Персонализированный контент

  • Cache-Control: private, max-age=60
  • Избегайте прокси-кешей — только браузер
  • Service Worker с явным управлением кешем

Производительность vs свежесть

Опытным путем установлено: 300 мс задержки в отклике API снижают конверсию на 7%. Но устаревшие данные могут привести к еще большим потерям. Баланс достигается через:

  1. Слои кеширования: CDN → инвертированный прокси (Varnish) → сервер → клиент
  2. Уровни свежести: user-specific (0-10 сек) | general data (1-5 мин) | static (1 год+)
  3. Мониторинг: отслеживание кеш-хитов на CDN, эвристика браузеров

Измерьте эффективность вашей стратегии в полевых условиях:

javascript
// На всех страницах отслеживаем производительность
const entry = performance.getEntriesByName(resourceUrl)[0];
if (entry) {
  const cacheStatus = entry.transferSize === 0 ? 'hit' : 'miss';
  analytics.send('cache_performance', {
    resource: resourceUrl,
    status: cacheStatus,
    duration: entry.duration
  });
}

Эволюция, а не революция

Внедряйте кеширование итеративно:

  1. Начните с заголовков cache-control для статики
  2. Добавьте ETag для критичных API эндпоинтов
  3. Внедрите Service Worker для основных страниц с offline-first подходом
  4. Мониторьте и оптимизируйте через автоматические метрики по типам ресурсов

Кеширование перестает быть хитростью для оптимизации и превращается в архитектурную необходимость. Грамотная реализация сокращает стоимость инфраструктуры на 60%, а пользователи получают моментальные загрузки даже при медленной сети. Релиз не заканчивается в production — дальнейшая настройка кеша уже на проде принесёт выигрыш каждому пользователю.