Service worker и офлайн
Что это такое
Service worker — это скрипт, который браузер выполняет в фоне, отдельно от любой страницы; он может перехватывать сетевые запросы вашего сайта и отвечать на них из кэша. Именно он стоит за поддержкой офлайна, мгновенными повторными визитами и фоновой синхронизацией. Доступа к DOM у него нет, и он живёт дольше страницы, которая его зарегистрировала.
Почему это важно
Service worker умеет ответить на запрос раньше, чем его увидит сеть, и потому это единственная возможность браузера заставить сайт работать при полном отсутствии соединения — и единственная, способная отдать устаревшую страницу пользователю, который ждёт свежую. Из-за этой второй половины поддержка офлайна становится решением дизайна, а не галочкой. Кэш без срока жизни и без инвалидации — баг с длинным запалом: он переживает развёртывания, а жалоба пользователя звучит как «сайт врёт», и ни в одном журнале ошибки при этом нет.
Ключевые идеи
Service worker — это прокси с жизненным циклом
Регистрация устанавливает скрипт, активация готовит его управлять страницами, и дальше он сидит между страницей и сетью. Новая версия устанавливается в фоне и вступает в силу, только когда старая отдаст управление; поэтому обновление порой выглядит так, будто ничего не произошло.
// sw.js
self.addEventListener("install", (event) => {
event.waitUntil(caches.open("v1").then((cache) => cache.addAll(["/", "/offline.html"])));
self.skipWaiting();
});
skipWaiting заставляет нового воркера взять управление сразу, а без него старый продолжает обслуживать, пока не закроется каждая управляемая вкладка.
Стратегия кэширования выбирается на ресурс, а не на сайт
Cache-first подходит хешированному ресурсу, который никогда не меняется; network-first нужен полезной нагрузке API, которая обязана быть свежей; stale-while-revalidate даёт мгновенный ответ и обновляет данные в фоне. Разным маршрутам одного приложения нужны разные стратегии, и главная работа здесь — правильно распределить их по маршрутам.
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/api/")) {
event.respondWith(fetch(event.request)); // network-first: всегда свежий
return;
}
event.respondWith(caches.match(event.request).then((hit) => hit || fetch(event.request)));
});
Кэшируйте оболочку и версионируйте её
Предварительное кэширование оболочки приложения делает второй визит мгновенным. Каждое развёртывание должно создавать новое имя кэша и удалять старое при активации, иначе релиз оставит в браузере два поколения ресурсов.
Не кэшируйте то, что кэшировать нельзя
Ответы API с аутентификацией и всё пользовательское не должно попадать в кэш, который прочитает следующий человек за этим устройством, и не должно отдаваться после выхода из системы. Держите в кэше статические ресурсы и публичные данные, пока тщательно не обдумали последствия.
Офлайн — это состояние, а не ошибка
Страница, упавшая с ошибкой браузера, выглядит сломанной, а страница, которая показывает понятное состояние офлайна с уже имеющимися данными, остаётся рабочей. Обращайтесь с ним как с обычным состоянием интерфейса — ровно так же, как с загрузкой или ошибкой.
Обновляйтесь осознанно
Service worker, который молча обновляется, пока пользователь на середине формы, приводит к потере данных. Покажите приглашение или обновляйтесь на очередном переходе — но решите это сами, а не оставляйте выбор поведению по умолчанию.
Практика
Заставьте страницу загрузиться с выключенной сетью
Зарегистрируйте service worker, который предварительно кэширует страницу-оболочку и её ресурсы, затем загрузите страницу, отключите сеть в браузере и перезагрузите.
Готово, когда: страница отрисовывается без сети, ресурсы приходят из кэша, и ни один запрос не доходит до панели Network.
Выпустите новую версию и посмотрите, как меняется кэш
Измените кэшированный ресурс, разверните проект и проследите, какую версию браузер отдаёт до и после того, как старый воркер отдаст управление.
Готово, когда: вы увидели однократную отдачу устаревшей версии и можете назвать событие, после которого управление взяла новая.
Частые ошибки
- Кэширование всего одной стратегией — ответ API отдаётся из кэша много позже того, как изменился; исправить: выбирайте стратегию на каждый маршрут.
- Имя кэша, которое никогда не версионируется — развёртывания оставляют старые ресурсы, и пользователи видят смесь; исправить: меняйте имя кэша на каждую сборку и удаляйте предыдущее при активации.
- Кэширование ответов с аутентификацией — личные данные достаются следующему человеку за устройством; исправить: держите пользовательские ответы вне кэша.
- Убеждение, что новый воркер берёт управление сразу — старый обслуживает, пока не отдаст управление; исправить: вызывайте
skipWaitingосознанно и обрабатывайте вызванную этим перезагрузку страницы. - Показ ошибки браузера в офлайне — приложение выглядит сломанным, а не отключённым; исправить: отрисовывайте состояние офлайна с тем, что доступно.
Что читать дальше
- JavaScript (02-frontend/06-javascript) — промисы и обработчики событий составляют здесь всю поверхность API.
- Устанавливаемость и манифест приложения (02-frontend/25-progressive-web-apps-pwa/02-installability-and-the-app-manifest) — вторая половина того, как сделать сайт устанавливаемым.
- Кэширование (Redis) (05-backend-data/16-caching-redis) — инвалидация — та же проблема на сервере, только с куда лучшим журналом ошибок.