Обновление SW и устаревший кеш

Урок разбирает самую болезненную проблему PWA: как обновить приложение и не застрять на старом кеше.

Устаревший кеш — ситуация, когда пользователь видит старую версию приложения, потому что Service Worker продолжает отдавать ресурсы из кеша предыдущей версии.

Почему кеш «застревает»

Главное удобство PWA — кеширование — оборачивается главной болью: вы выкатили новую версию, а пользователь по-прежнему видит старую. Причины две. Первая: ваш Service Worker по cache-first отдаёт старые файлы из кеша и не идёт в сеть. Вторая: новый Service Worker сам по умолчанию ждёт в состоянии waiting, пока открыты вкладки со старым. В результате обновление «не доезжает».

Версионирование кеша

Базовый приём — версия в имени кеша. При каждом релизе меняете версию, кешируете заново, а старый кеш чистите на activate:

const CACHE = 'app-v3';  // подняли версию при релизе

self.addEventListener('activate', function (event) {
  event.waitUntil(
    caches.keys().then(function (keys) {
      return Promise.all(
        keys.filter(function (k) { return k !== CACHE; })
            .map(function (k) { return caches.delete(k); })
      );
    })
  );
});

Как браузер замечает новый SW

Браузер периодически и при перезагрузке скачивает файл sw.js и сравнивает его байт в байт со старым. Если есть отличие хоть на байт — это «новый» Service Worker, запускается его установка. Поэтому важно: сам файл sw.js не должен агрессивно кешироваться сервером, иначе браузер не увидит изменений. Обычно для него ставят короткий или нулевой срок кеширования.

релиз --> sw.js изменился --> браузер качает новый --> install
  --> новый SW в waiting (старый ещё работает)
  --> закрыли все вкладки ИЛИ skipWaiting --> activate --> чистка старых кешей

skipWaiting и уведомление пользователя

Чтобы новая версия активировалась быстрее, используют self.skipWaiting(). Но активировать новый Service Worker «под пользователем» в момент работы рискованно (могут разъехаться версии ресурсов). Грамотный паттерн: новый Service Worker встаёт в waiting, приложение показывает плашку «Доступно обновление — обновить?», и только по клику пользователя посылает воркеру сообщение вызвать skipWaiting и перезагружает страницу.

// в SW
self.addEventListener('message', function (e) {
  if (e.data === 'SKIP_WAITING') self.skipWaiting();
});
// на странице: при обнаружении waiting SW показать кнопку «Обновить»,
// по клику: reg.waiting.postMessage('SKIP_WAITING'); затем перезагрузка

Частые ошибки

  • Не менять имя кеша при релизе. Старые файлы живут вечно — обновление не видно.
  • Кешировать sw.js на сервере надолго. Браузер не увидит новую версию воркера.
  • Бездумный skipWaiting. Версии ресурсов могут разъехаться; лучше обновлять по согласию пользователя.
  • Не чистить старые кеши. Память сайта раздувается от версии к версии.

Итоги

  • Устаревший кеш — главная боль PWA: пользователь видит старую версию.
  • Решение: версионировать имя кеша и чистить старые на activate.
  • Файл sw.js не должен надолго кешироваться сервером.
  • Обновление лучше предлагать пользователю кнопкой, а не навязывать skipWaiting.
Проверьте себя
1. Какой приём помогает чистить устаревший кеш при релизе?
AОтключить Service Worker
BВерсионировать имя кеша (app-v1 → app-v2) и удалять старые кеши на activate
CИспользовать только localStorage
DОтключить HTTPS
2. Почему файл sw.js не стоит надолго кешировать на сервере?
AОн слишком большой
BИначе браузер не увидит изменений в нём и не запустит установку нового Service Worker
CЭто нарушает HTTPS
DМанифест перестанет работать
3. Почему бездумный skipWaiting может быть опасен?
AОн удаляет манифест
BНовый Service Worker активируется под работающим пользователем, и версии ресурсов могут разъехаться
CОн отключает офлайн навсегда
DОн требует App Store