awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Граничные случаи service worker: version skew, долговременность и ловушка навигации

Суть Version skew из-за не хешированных ресурсов, ловушка kill-and-restart долговременности, и почему сломанный service worker, перехватывающий навигацию, — это инцидент класса stop-the-deploy.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

Вы деплоите service worker, который раздаёт кеш app shell по стратегии cache-first. Через неделю деплоите исправление бага. Часть пользователей никогда его не получит — они продолжают получать сломанный закешированный shell при каждой перезагрузке, и никакой кнопки восстановления для обычного пользователя не существует.

Проблема обновления и version skew

Кеш ресурсов service worker привязан к версии его кода. При деплое версии N+1 открытая страница может работать с воркером версии N, пока HTML версии N+1 уже задеплоен — или наоборот. Если воркер раздаёт cache-first app.js версии N, а HTML ожидает app.js версии N+1, получается runtime-ошибка из-за несовпадающих модулей.

Надёжный паттерн:

  1. Content-hash каждое имя файла ресурса (app.4f3a1c.js). Старые и новые ресурсы сосуществуют в кеше без коллизий.
  2. Version-tag имена кешей (cache-v3, cache-v4). Pre-cache весь набор ресурсов каждого деплоя под version tag.
  3. В activate удалять только устаревшие кеши — кеши, чей version tag не является текущим. Делайте это после clients.claim(), чтобы ни одна контролируемая страница не потеряла ресурсы в середине сессии.
  4. Раздавать навигационные запросы network-first (или по выделенному маршруту app-shell), чтобы пользователи всегда получали HTML, консистентный с активным воркером.

Тот же класс бага применим к sw.js самому по себе: браузеры кешируют файл воркера по умолчанию до 24 часов. Современная практика — отдавать sw.js с заголовком Cache-Control: no-cache, чтобы браузер всегда перепроверял его на навигации.

Service workers не являются долговременными

Браузер агрессивно убивает idle service worker — часто в течение секунд после завершения fetch-события — и перезапускает при следующем событии. Любое состояние в module-level переменной исчезает при перезапуске. Это частый источник багов:

  • Счётчик запросов в полёте.
  • Кеш ожидающих промисов.
  • WebSocket-соединение в глобальной переменной.

Всё испаряется. Постоянное состояние должно жить в IndexedDB или Cache API.

Долгоживущая работа внутри обработчика события должна быть обёрнута в event.waitUntil(promise) — это говорит браузеру: «не убивай меня, пока этот промис не завершится». Забыть waitUntil — значит браузер может завершить воркер на полпути, и background sync, push-обработка, заполнение кеша молча не завершатся.

Факты о долговременности service worker
Время убийства idle-воркера
Секунды после последнего события
HTTP-кеш браузера для sw.js
До 24 часов по умолчанию
Рекомендуемый заголовок для sw.js
Cache-Control: no-cache
Варианты постоянного состояния
Только Cache API или IndexedDB
Забытый waitUntil → тихий сбой
push, sync, заполнение кеша

Перехват навигации и опасность app-shell

Самый мощный — и самый опасный — паттерн service worker: перехват навигационных запросов. fetch-обработчик ловит запрос на HTML-документ и возвращает закешированный app shell. Это даёт мгновенную загрузку, но создаёт класс багов, иначе невозможных.

Ловушка: если вы задеплоите баг в app shell и закешируете его с cache-first, каждый повторный визит будет раздавать сломанный shell из кеша, минуя сеть, где лежит исправление. Пользователь не может выбраться обычной перезагрузкой.

Защита многоуровневая:

  1. Навигационные запросы — network-first с коротким таймаутом (~3 с, с откатом на кеш). Это гарантирует, что исправление дойдёт до пользователей при первой успешной загрузке.
  2. Держите kill switch — версионированный эндпоинт, который воркер проверяет при activate или периодически. По сигналу вызывает self.registration.unregister() и удаляет кеши. Это позволяет удалённо отсоединить сломанный service worker от всех клиентов.
  3. Никогда не кешировать навигацию только из кеша. Всегда должен быть сетевой путь.

Сломанный service worker, задеплоенный широко — это инцидент stop-the-deploy, потому что обычные пользователи не имеют кнопки восстановления: они не могут открыть DevTools, не могут очистить данные сайта. Единственный выход — kill switch или свежий деплой, который старый воркер подхватит при следующей активации.

Викторина

Service worker хранит карту запросов в полёте в module-level переменной `const cache = new Map()`. Через несколько секунд простоя записи исчезают. Почему?

Викторина

Вы деплоите обновление service worker, и часть пользователей сообщает о сломанной странице: скрипты не загружаются с ошибками module-mismatch. Какова наиболее вероятная причина и надёжное исправление?

Викторина

Пользователь застрял на сломанном закешированном app shell, и обычная перезагрузка не помогает. Какой механизм восстановления нужно было заложить заранее?

Почему это работает

Почему сломанный service worker так сложно исправить? Когда service worker перехватывает навигацию, он сидит между браузером и сервером для самого HTML-документа — страница не может загрузиться без ответа воркера первым. В отличие от сломанного CDN (где браузер откатывается на origin), сломанный service worker успешно отвечает сломанным закешированным ответом. Браузер не может отличить правильный кешированный ответ от ошибочного. Вот почему kill switch должен быть проактивным: URL, который воркер проверяет на каждой активации, чей ответ говорит воркеру, нужно ли ему отменить регистрацию. Если вы ждёте сообщений от пользователей — вы уже задеплоили.

Вспомните перед уходом
  1. 01
    Почему module-level состояние service worker исчезает между запросами?
  2. 02
    Каков failure mode version skew в service workers и как его предотвратить?
  3. 03
    Почему сломанный service worker, перехватывающий навигацию — инцидент stop-the-deploy, и какова архитектурная защита?
Итог

У service workers три основных failure mode. Version skew: раздача кешированных ресурсов неверной версии — предотвращается content-hashed именами файлов и version-tagged кешами. Ловушка долговременности: module-level состояние испаряется между событиями, потому что браузер убивает idle воркеры; используйте event.waitUntil для долгих операций и IndexedDB/Cache API для состояния. Перехват навигации: кеширование самого HTML-документа означает, что сломанный shell навсегда удерживает пользователей — всегда используйте network-first для навигации и стройте kill-switch эндпоинт. Все три сбоя становятся трудноотменяемыми продакшн-инцидентами без превентивных мер. Файл sw.js должен отдаваться с Cache-Control: no-cache — иначе обновление воркера может задержаться до 24 часов.

Связанные уроки
встречается в143
Продолжить восхождение ↑Пулы воркеров, Comlink и наблюдаемость в продакшене
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.