В предыдущей главе мы написали обработчик fetch, который смотрит сначала в кэш и только потом идёт в сеть. Это всего одна из стратегий кэширования — распространённая, но далеко не единственная. На реальном сайте разные ресурсы кэшируют по-разному: для статических CSS подходит одно, для API-ответов — другое, для аватарок — третье. Разберём пять основных стратегий и подскажем, какую брать под какой случай.

Перед стратегиями — Cache API

Кэш в service worker — это не браузерный HTTP-кэш, а отдельное хранилище с собственным JS-API. Это одно из нескольких браузерных хранилищ — полный обзор всех типов есть в мануале про типы хранилищ в браузере. Здесь — только то, что нужно для service worker. Главные методы:

  • caches.open(name) — открывает (создаёт, если нет) именованный кэш.
  • cache.add(url), cache.addAll([urls]) — кладёт ресурс по URL в кэш, делая запрос в сеть.
  • cache.put(request, response) — кладёт готовую пару запрос/ответ в кэш.
  • cache.match(request) — ищет ответ для данного запроса.
  • cache.delete(request) и caches.delete(name) — удаление отдельной записи или всего кэша.

Этих пяти методов достаточно для любой стратегии. Всё остальное — это разные способы их комбинировать.

Cache First — сначала кэш, потом сеть

Самая частая стратегия. Если файл есть в кэше — отдаём из кэша, не идём в сеть. Если нет — запрашиваем, сохраняем, возвращаем.

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        return caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});

Когда подходит: для ресурсов, которые редко меняются и имеют версию в имени файла (хэш в названии бандла, например app.a3b9f4c.css). Если содержимое поменяется — поменяется и имя файла, и старый кэш просто не будет запрашиваться.

Когда не подходит: для HTML страниц и API-ответов, которые могут поменяться между визитами.

Network First — сначала сеть, кэш как запасной

Сначала идём в сеть. Получили ответ — кладём в кэш, отдаём странице. Не получили (нет сети, таймаут) — берём из кэша.

self.addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        const copy = response.clone();
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Когда подходит: для HTML-страниц и API-ответов, где свежесть важнее скорости. Пользователь онлайн — всегда видит актуальное, ушёл в офлайн — видит последнюю сохранённую копию.

Минус: на медленной сети загрузка ощущается медленной — пока не истечёт таймаут запроса, страница ждёт. Можно добавить искусственный setTimeout на 3 секунды и при превышении отдавать из кэша, не дожидаясь сети.

Stale While Revalidate — устаревшее, но мгновенное

Самая хитрая. Если в кэше что-то есть — немедленно отдаём это пользователю. Одновременно идём в сеть, получаем свежий ответ и тихо обновляем кэш. Следующий запрос вернёт уже новую версию.

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((cached) => {
        const networkFetch = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || networkFetch;
      });
    })
  );
});

Когда подходит: аватарки, картинки превью, новостные ленты — всё, где «немного устаревшее» не страшно, а мгновенность важна.

Минус: пользователь видит устаревший контент до следующего обновления страницы. Для критичных данных (баланс, статус заказа) не подходит.

Network Only и Cache Only

Две крайности, которые редко используются как глобальные стратегии, но удобны для отдельных типов запросов внутри сложного service worker.

Network Only — всегда идём только в сеть, кэш не трогаем. Подходит для запросов авторизации, оплаты, любых операций, где недопустимо отдать устаревший ответ:

if (request.url.includes("/api/payment/")) {
  event.respondWith(fetch(event.request));
  return;
}

Cache Only — всегда из кэша, в сеть не ходим даже если файла нет. Подходит для ресурсов, которые точно были предзагружены на этапе install и не могут отсутствовать (логотип, базовый CSS).

if (request.url.endsWith("/logo.svg")) {
  event.respondWith(caches.match(event.request));
  return;
}

Шпаргалка: какая стратегия под какой ресурс

Тип ресурса Стратегия Почему
Хэшированные бандлы JS/CSS (app.a3b9.js) Cache First Имя файла меняется при изменении содержимого — промахов кэша не будет.
HTML страниц Network First Контент может меняться, нужна актуальность; офлайн — покажет последнюю версию.
API-ответы (списки, ленты) Stale While Revalidate Мгновенно показываем, тихо обновляем в фоне.
API-ответы (платежи, авторизация) Network Only Устаревший ответ недопустим.
Аватарки, превью Stale While Revalidate Лёгкое устаревание не критично.
Логотип, шрифты, иконки Cache First Меняются раз в год — нет смысла каждый раз идти в сеть.
Фолбэк offline.html Cache Only Гарантированно лежит в кэше с этапа install.

Workbox — когда писать руками не хочется

Все примеры выше — ручные. Они хорошо подходят, пока сайт небольшой. Но как только стратегий становится три-четыре, появляются версии кэшей, нужна автоматическая очистка по возрасту записей — код service worker превращается в простыню на 300 строк, где легко ошибиться.

Для таких случаев есть библиотека Workbox от команды Chrome. Она предоставляет готовые стратегии, маршрутизатор по URL-паттернам и автоматическую генерацию precache-манифеста под бандл фронтенда.

import { registerRoute } from "workbox-routing";
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from "workbox-strategies";

// Хэшированный JS / CSS — Cache First
registerRoute(
  ({ request }) => request.destination === "script" || request.destination === "style",
  new CacheFirst({ cacheName: "static-assets" })
);

// HTML страниц — Network First
registerRoute(
  ({ request }) => request.mode === "navigate",
  new NetworkFirst({ cacheName: "pages" })
);

// Картинки — Stale While Revalidate
registerRoute(
  ({ request }) => request.destination === "image",
  new StaleWhileRevalidate({ cacheName: "images" })
);

То, что в ручном service worker заняло бы 80 строк, в Workbox умещается в 15. Плюс плагины: лимит на размер кэша, время жизни записей, фоновая синхронизация при возврате сети, аналитика. Большинство production-PWA используют именно Workbox, а не написанный руками sw.js.

На этапе обучения полезно сначала пописать стратегии руками — чтобы понимать, что Workbox делает внутри. На реальном проекте сразу берите Workbox.