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 осознанно и обрабатывайте вызванную этим перезагрузку страницы.
  • Показ ошибки браузера в офлайне — приложение выглядит сломанным, а не отключённым; исправить: отрисовывайте состояние офлайна с тем, что доступно.

Что читать дальше