PWA обещает много, и от этого рождаются мифы. Progressive Web App - не фреймворк и не галочка в сборщике, которая разом делает приложение устанавливаемым и работающим без сети. Это набор браузерных возможностей и подход к их применению. В основе - обычное веб-приложение: те же URL, HTTP, семантика, доступность, progressive enhancement. PWA это не отменяет, а надстраивается сверху.
Польза складывается из трёх свойств. Надёжность - основной путь не разваливается при плохой сети. Устанавливаемость - приложение живёт в лаунчере и открывается в отдельном окне. Интеграция - доступ к иконкам, шорткатам и уведомлениям ОС. Отвечают за это разные механизмы: Web App Manifest - за интеграцию с системой, Service Worker - за программируемый сетевой слой и фоновые события, Cache Storage и IndexedDB - за локальные данные. Ни один сам по себе не делает продукт offline-first.
Естественно предположить обратное: подключил манифест, зарегистрировал service worker - и приложение как будто стало офлайновым. Иллюзию подкрепляют инструменты: плагин генерирует worker, Lighthouse ставит галочку, DevTools показывает регистрацию. Но worker, который просто есть, ничего не гарантирует. Он влияет на надёжность лишь когда осознанно перехватывает нужные запросы и обслуживает их из кеша. Пустой worker - украшение, а не отказоустойчивость.
Всё ломается на неверной модели сети. Сеть - внешний сервис с переменной задержкой, а не переключатель есть/нет. Отсюда главная ловушка - navigator.onLine. Свойство говорит лишь, что у браузера есть хоть какое-то подключение. Оно не доказывает, что отработал DNS, установился TLS, что вы не за captive-порталом и что ваш API отвечает. Устройство в метро формально онлайн, но каждый запрос отваливается по таймауту.
Полезнее держать в голове, что именно доказывает каждый сигнал - и, важнее, чего он не доказывает.
| Сигнал | Что он доказывает | Чего не доказывает |
|---|---|---|
| navigator.onLine | У браузера есть некоторое сетевое соединение | Что работают DNS, TLS, captive portal и ваш API |
| fetch('/health') | Конкретный endpoint ответил сейчас | Что следующий запрос пройдёт |
| Timeout / ошибка | Операция не подтверждена клиентом | Что сервер точно не выполнил мутацию |
|---|
Отсюда offline-first как модель отказов. Интерфейс сначала работает с локальным состоянием, а сеть служит для доставки и согласования изменений. Это не строгий local-first: сервер остаётся источником истины, но его недоступность не блокирует каждое действие. Возьмём приложение заметок Offline Notes, наш пример на всю книгу. Пользователь пишет заметку в поезде без связи. Правильно: она сразу сохраняется локально, помечается как ожидающая и уходит на сервер, когда сеть вернётся. Неправильно: спиннер и ошибка.
Чтобы это работало, отказ проектируют, а не ловят постфактум. Полезно заложить четыре режима: online - сеть отвечает нормально; degraded - медленно и ненадёжно; offline - не отвечает вовсе; reconnecting - связь только что вернулась. И состояние показывают не глобально, а по каждой операции. Одна зелёная точка в шапке бесполезна: она молчит про конкретную заметку. Нужен статус записи - сохранено локально, ожидает отправки, синхронизировано, конфликт.
Разница не косметическая. Пусть Offline Notes доверяет navigator.onLine и, увидев true, сразу шлёт POST без локальной записи. Пользователь в зоне слабого сигнала жмёт сохранить, запрос уходит в таймаут - и текст теряется, durable-копии не было. В offline-first заметка уже в хранилище, отправка откладывается, пользователь ничего не замечает. Оффлайн нужен не каждому продукту, но где данные создают в дороге, это разница между инструментом и демкой. Share Target, шорткаты и уведомления - лишь дополнение поверх надёжного ядра, а не замена.