В предыдущей главе мы написали обработчик 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.