Вся книга сжимается до одной фразы: сеть может пропасть на любой строке, данные пользователя - нет. Возьмём это конкретно. Пользователь жмёт "Создать заказ", запрос доходит до сервера, сервер фиксирует заказ - и ответ теряется на обратном пути. Клиент видит ошибку. Как повторить запрос, не создав второй заказ? Всё остальное в offline-first - строительные леса вокруг этого вопроса, а хороший ответ собирается из нескольких решений в правильном порядке.
Начинать стоит не с "давайте добавим service worker", а с ценности. Для каждого сценария честно назовите, чего стоит offline: курьер, потерявший сигнал в подвале, обязан по-прежнему видеть маршрут и отметить точку - здесь offline критичен; статичная маркетинговая страница почти ничего от него не выигрывает. Формулировка ценности решает, сколько механики заслуживает конкретный экран.
Дальше разложите всё, к чему прикасается приложение, на четыре рода данных - каждый требует своего. Ассеты (оболочка приложения, JS, CSS) версионируются хэшем и предзагружаются в precache. Снимки (отрендеренный список, который вы уже получили) - это кэшируемые представления, их не страшно показать устаревшими. Сущности (сами записи заказов) живут в IndexedDB как локальный источник истины. Команды (создать заказ, отменить) - это намерения пользователя, которые обязаны пережить перезагрузку и мёртвую сеть. Спутать команду со снимком - корень большинства потерь данных.
Для каждого запроса примите два решения явно. Freshness - насколько устаревший ответ допустим: cache-first для оболочки, network-first там, где нужен баланс свежести и доступности, stale-while-revalidate для списков, которые терпят секунду устаревания ради мгновенного показа. И failure semantics - что видит пользователь, когда сервер недостижим: пустой экран, устаревшие данные с пометкой или явная ошибка. Именно на размытых умолчаниях offline-приложения и подгнивают; политику стоит записать поэндпоинтно.
Правило безопасности для записи: сначала durable-транзакция в локальном хранилище, потом оптимистичный UI. Заказ ложится в outbox в IndexedDB до анимации кнопки; умрёт вкладка - намерение уцелеет. А потерянный ответ закрывается на сервере: у каждой команды свой idempotency-ключ, чтобы повтор запроса, который сервер уже обработал, вернул тот же результат, а не второй заказ. Сюда - версионирование и понятный UX конфликтов на случай, когда две правки одной сущности гонятся друг за другом.
И только теперь добавляйте Background Sync - строго как оптимизацию поверх переднего плана, который уже работает. Background Sync - это подсказка от браузера, что связь вернулась, а не гарантия и не общедоступная возможность. Если outbox опустошается только по этому событию, на платформах без него записи зависают навсегда. Пол - это flush переднего плана: при открытии приложения и по событию online; Background Sync сверху лишь премия, когда он есть.
Update lifecycle и rollback проектируйте до первого деплоя, а не после первого инцидента: коротко кэшируемый sw.js, неизменяемые захэшированные ассеты, совместимость N и N-1, готовый kill switch. И проверяйте переходы состояний на реальных целевых устройствах, а не в эмуляторе на быстром Wi-Fi: режим полёта посреди записи, принудительное обновление при открытой старой вкладке, вытеснение по quota. Именно переходы - а не галочки в списке - и есть то место, где offline-first по-настоящему выигрывается или проигрывается.