2.04. Фоновые процессы и работа без интернета
Фоновые процессы и работа веб-приложений без интернета
Традиционная веб-страница представляет собой статический или динамически сгенерированный документ, жизненный цикл которого ограничен временем открытия вкладки до её закрытия. Выполнение JavaScript-кода полностью зависит от активности окна браузера: при сворачивании вкладки или переходе на другой сайт большинство задач приостанавливается, а при закрытии — полностью прекращается. Это соответствует изначальной парадигме World Wide Web: клиент делает запрос — сервер отвечает — клиент отображает результат. Связь однократна, состояние — эфемерно, автономность — невозможна.
Современные веб-приложения, однако, стремятся к уровню функциональности, привычному в нативных средах: работа в офлайне, фоновая загрузка, push-уведомления, фоновая синхронизация изменений, обновление содержимого без участия пользователя. Такие требования выходят за рамки классической клиент-серверной модели и требуют фундаментального пересмотра архитектуры выполнения кода на стороне клиента. Здесь ключевым становится понятие автономности, а техническим фундаментом — фоновые процессы.
Что такое автономность в контексте веб-приложений?
Автономность — это способность приложения функционировать при отсутствии стабильного или вообще любого сетевого соединения с сервером. Это полноценное выполнение бизнес-логики: редактирование данных, навигация по интерфейсу, локальная валидация, реакция на действия пользователя — всё это должно происходить без задержек, вызванных ожиданием ответа от сервера.
Автономность подразумевает временную декомпозицию взаимодействия. Приложение продолжает работать, накапливая изменения локально; в моменты, когда связь восстанавливается, оно автоматически или по инициативе пользователя согласует своё состояние с сервером. Таким образом, автономность — это не альтернатива онлайн-режиму, а его надстройка, обеспечивающая устойчивость к сетевым сбоям и повышающая воспринимаемую отзывчивость.
Степень автономности может варьироваться:
- Частичная — доступна только часть функционала (например, чтение ранее загруженных статей, но не публикация комментариев).
- Полная — все ключевые операции возможны локально, включая создание, редактирование и удаление сущностей; синхронизация происходит прозрачно для пользователя.
Достижение автономности требует комплексного подхода, затрагивающего хранилище, сетевую логику, управление состоянием и интерфейс. Основной механизм, делающий это возможным в современном вебе, — вынос части логики выполнения за пределы жизненного цикла страницы.
Фоновые процессы
Фоновый процесс — это задача, выполняемая браузером независимо от активности вкладки и даже после её закрытия. Это принципиальный сдвиг: JavaScript, изначально ограниченный sandbox’ом документа, получает возможность «жить» дольше самого документа.
Такая возможность не появилась внезапно. Её предпосылками стали:
- Повсеместное распространение мобильных устройств с нестабильными и дорогими каналами связи.
- Рост требований к UX: пользователи ожидают мгновенного отклика, как в нативных приложениях.
- Ограничения push-уведомлений и фоновой загрузки в нативных веб-приложениях (PWA), которые требовали стандартизированных, безопасных и энергоэффективных решений.
Важно чётко разделять фоновую работу от псевдо-фоновых практик:
setTimeout/setIntervalс большими задержками не являются фоновыми: они приостанавливаются при неактивности вкладки.requestIdleCallbackилиvisibilitychangeпозволяют отложить выполнение, но не гарантируют его после закрытия вкладки.- Web Workers работают в отдельном потоке, но их жизненный цикл всё равно привязан к странице-родителю.
Настоящая фоновая работа в вебе возможна только через Service Worker и API, построенные на его основе. Service Worker — это скрипт, зарегистрированный браузером как системный обработчик событий, связанных с сетью, временем, push-каналами и фоновыми задачами. Он запускается по событию (например, входящий push, таймер, необходимость синхронизации), выполняет необходимые действия и завершается. Такой подход минимизирует энергопотребление и предотвращает неограниченное выполнение произвольного кода.
Синхронизация
Синхронизация — центральное понятие в архитектуре автономных приложений. Она представляет собой процесс достижения согласованности данных между локальным клиентским состоянием и удалённым серверным состоянием.
Синхронизация не всегда подразумевает немедленный обмен. Её можно классифицировать по нескольким измерениям:
По направлению:
- Однонаправленная (push/pull) — данные обновляются только с сервера на клиент (например, обновление новостей) или только с клиента на сервер (например, сохранение черновика).
- Двунаправленная — изменения возможны с обеих сторон; требуется механизм разрешения конфликтов (например, два пользователя редактируют один документ).
По времени:
- Мгновенная (real-time) — изменения передаются и применяются сразу (WebSockets, SSE). Требует постоянного соединения.
- Отложенная (deferred) — действия ставятся в очередь и выполняются при удобном случае: при восстановлении сети, при следующем запуске приложения, в фоне. Именно отложенная синхронизация лежит в основе автономной работы.
По инициатору:
- Пользовательская — синхронизация запускается по явному действию (кнопка «Сохранить и отправить»).
- Системная — происходит автоматически, без участия пользователя, по расписанию или по событию (подключение к Wi-Fi, тихий период).
Важнейшее свойство устойчивой синхронизации — идемпотентность операций. Клиент должен иметь возможность повторно отправить одно и то же действие (например, «сохранить заказ №123»), не опасаясь дублирования на сервере. Это достигается через уникальные идентификаторы операций, версионирование сущностей или применение операций в виде diff-патчей.
Автономное приложение обязано вести учёт состояния синхронизации для каждой сущности: «сохранено локально», «ожидает отправки», «отправлено, ожидает подтверждения», «синхронизировано», «обнаружен конфликт». Такая метаинформация хранится вместе с данными и управляет поведением интерфейса (например, серый значок галочки превращается в зелёный после подтверждения сервером).
Service Worker и фоновые API
Service Worker
Service Worker — промежуточный прокси-обработчик, встроенный в стек сетевых вызовов браузера. После регистрации и активации он перехватывает все исходящие fetch-запросы от страницы, а также получает события от системных API: push, notificationclick, sync, backgroundfetchsuccess и др.
Важнейшие особенности Service Worker’а:
- Работает в отдельном глобальном контексте, изолированном от
window. Нет доступа к DOM,localStorage, синхронным XHR. - Запускается по событию, а не постоянно. После обработки события он «усыпляется», что экономит ресурсы.
- Может быть активен, даже когда ни одна вкладка приложения не открыта — если есть ожидающие события (например, push-уведомление или отложенная синхронизация).
- Имеет доступ к IndexedDB, Cache API, Notifications API, Push API, Background Sync API — то есть ко всем инструментам, необходимым для автономной работы.
Service Worker не делает приложение автономным сам по себе, он предоставляет платформу, на которой строятся решения: кэширование ресурсов, перехват запросов к API, постановка задач в очередь синхронизации, реакция на сетевые изменения. Реализация логики автономности — задача разработчика приложения.
Service Worker не является «демоном», постоянно висящим в памяти. Его поведение строго регламентировано событийной моделью и политиками энергосбережения. Понимание жизненного цикла критично для корректной реализации фоновых задач.
Регистрация происходит с помощью navigator.serviceWorker.register(). Браузер скачивает скрипт, проверяет его синтаксис и запускает в отдельном потоке. На этом этапе ещё нет перехвата сетевых запросов — Service Worker находится в состоянии installing.
Установка (install) — фаза, в которой приложение может подготовить начальное состояние: создать кэш, инициализировать базу данных, загрузить статические ресурсы. Обработчик события install должен вызвать event.waitUntil(), передав промис, завершение которого сигнализирует об успешной установке. Пока этот промис не разрешён, Service Worker остаётся в состоянии installing и не активируется. Это позволяет гарантировать, что все необходимые ресурсы доступны до начала перехвата запросов.
Активация (activate) происходит после завершения установки и после того, как все вкладки, использующие предыдущую версию Service Worker’а, будут закрыты (если не вызван skipWaiting()). На этапе активации обычно производится миграция данных — очистка устаревших кэшей, обновление схем IndexedDB. После активации Service Worker получает статус active и начинает перехватывать сетевые события.
Выполнение по событию — основной режим работы. Service Worker «просыпается» только при наступлении события:
fetch— исходящий сетевой запрос от страницы;push— получение push-сообщения от сервера;notificationclick— клик по уведомлению;sync— событие фоновой синхронизации;backgroundfetchsuccess/backgroundfetchfail— завершение фоновой загрузки.
После завершения обработчика события Service Worker, если у него нет ожидающих асинхронных задач (например, незавершённых промисов, зарегистрированных через event.waitUntil()), немедленно терминируется. Это гарантирует, что произвольный код не будет работать в фоне бесконтрольно.
Изоляция Service Worker’а выражается не только в отсутствии доступа к DOM. Он работает в своём собственном глобальном контексте (self), имеет отдельный event loop, не разделяет память с вкладками, и не может напрямую взаимодействовать с ними. Обмен данными возможен только через:
postMessage()— явная передача сообщений между страницей и Service Worker’ом;- косвенное взаимодействие через общие хранилища (IndexedDB, Cache API);
- инициирование событий на странице (например, отправка уведомления, на которое пользователь может кликнуть).
Эта изоляция предотвращает утечку сессий, кражу данных через вредоносные скрипты и несанкционированное потребление ресурсов.
Фоновая синхронизация (Background Sync)
Background Sync API решает задачу: «Как гарантировать, что действие пользователя будет выполнено, даже если он закрыл приложение до завершения отправки?»
Без этого API классическая реализация выглядит так: пользователь нажимает «Отправить», приложение делает fetch(), и если соединение нестабильно — запрос падает с ошибкой, интерфейс показывает «Не удалось отправить», и пользователь вынужден повторять действие. Это нарушает принцип «предварительного подтверждения» (optimistic UI), снижает доверие к приложению и ухудшает UX.
Background Sync позволяет сделать шаг вперёд: после нажатия «Отправить» приложение немедленно обновляет интерфейс (например, добавляет сообщение в чат с серой галочкой) и регистрирует задачу синхронизации. Даже если вкладка будет закрыта, браузер сохранит эту задачу и выполнит её в фоне, как только появится интернет.
Техническая последовательность:
- Приложение регистрирует sync-метку через
navigator.serviceWorker.ready.then(reg => reg.sync.register('send-message')).
Метка ('send-message') — это идентификатор типа задачи, который будет передан Service Worker’у в событииsync. - Service Worker обрабатывает событие
syncс соответствующей меткой. Внутри обработчика он извлекает данные из локального хранилища (например, из очереди сообщений в IndexedDB), отправляет их на сервер, и по результату обновляет статус в базе («отправлено»). - Если отправка завершается неудачей (например, сервер недоступен), Service Worker не подтверждает обработку события — и браузер автоматически повторит попытку позже, с экспоненциальной задержкой.
Background Sync — это pull со стороны клиента. Сервер не инициирует синхронизацию; она запускается клиентом по событию (регистрация метки) и выполняется браузером по своему усмотрению, с учётом состояния сети, заряда батареи и политики энергосбережения.
Требования и ограничения
- Доступен только в безопасном контексте (
httpsилиlocalhost). - Требует наличия активного Service Worker’а.
- Не гарантирует момент выполнения — браузер может отложить синхронизацию на часы или даже дни, если устройство в режиме энергосбережения.
- На мобильных устройствах (особенно iOS) реализация может быть ограничена: Safari поддерживает Background Sync только начиная с версии 16.4, и даже там поведение менее предсказуемо, чем в Chromium.
- Каждая метка синхронизации уникальна в рамках одного Service Worker’а. Повторная регистрация той же метки обновляет существующую задачу(если она ещё не выполнена).
Практическое использование
Рекомендуется использовать Background Sync для группировки логически связанных действий. Например, вместо регистрации отдельной синхронизации на каждое сообщение в чате — накапливать сообщения в локальной очереди и запускать одну синхронизацию при первом добавлении или при выходе из приложения. Это снижает нагрузку на сеть и упрощает обработку ошибок.
Фоновое извлечение (Background Fetch)
Background Fetch API расширяет идею фоновой синхронизации, но в обратном направлении: массовая загрузка данных с сервера, даже когда приложение неактивно.
Он решает задачи:
- Предзагрузка контента для офлайн-доступа (выпуски подкастов, эпизоды сериала, обновления справочников).
- Загрузка больших файлов без риска прерывания при сворачивании приложения.
- Обновление кэша в фоне без блокировки UI.
Принцип работы:
- Приложение инициирует фоновую загрузку через
reg.backgroundFetch.fetch('podcast-ep123', [request1, request2], options).
Можно передать массив запросов (например, аудиофайл + JSON-описание), а также параметры: отображать ли прогресс в уведомлении, можно ли использовать мобильный интернет. - Браузер берёт управление на себя: он показывает системное уведомление с прогресс-баром, управляет повторными попытками при обрыве, учитывает политики трафика (например, не загружать по мобильной сети без разрешения).
- По завершении (успешном или нет) браузер запускает Service Worker и передаёт ему событие
backgroundfetchsuccessилиbackgroundfetchfail. - В обработчике Service Worker получает доступ к загруженным данным через
event.fetches, проверяет их целостность, сохраняет в IndexedDB или Cache Storage, и может отправить пользователю итоговое уведомление («Эпизод готов к прослушиванию»).
Ключевое преимущество перед ручной реализацией через fetch + Service Worker — делегирование управления жизненным циклом загрузки браузеру. Разработчику не нужно:
- вручную обрабатывать обрывы соединения,
- управлять повторными запросами,
- синхронизировать прогресс между вкладками,
- беспокоиться о том, убит ли процесс при сворачивании.
Background Fetch — это «загрузка как сервис»: надёжная, наблюдаемая, энергоэффективная.
Ограничения
- Поддерживается только в Chromium-браузерах (Chrome, Edge, Opera). Firefox и Safari — не реализованы.
- Требует явного разрешения на показ уведомлений (поскольку прогресс отображается в них).
- Объём загружаемых данных может быть ограничен браузером (обычно — до нескольких гигабайт, но конкретные лимиты зависят от устройства и ОС).
- Нельзя получить прямой доступ к данным до обработки события в Service Worker’е — они хранятся во временной зоне, защищённой от прямого чтения страницей.
Безопасность и согласие пользователя
Фоновая работа расширяет возможности веб-платформы, но одновременно усиливает риски: несанкционированное потребление трафика, энергии, слежка за поведением пользователя. Поэтому все ключевые API работают только при соблюдении строгих условий:
- Безопасный контекст (
HTTPS). Исключение —localhostдля разработки. - Явное согласие на уведомления. Push-уведомления и прогресс фоновой загрузки требуют вызова
Notification.requestPermission(). Без разрешения — никаких фоновых событий, связанных с уведомлениями. - Пользовательский жест. Регистрация Background Sync или Background Fetch должна происходить в контексте действия пользователя (клик, нажатие Enter). Автоматическая регистрация при загрузке страницы запрещена.
- Прозрачность. Браузеры обязаны предоставлять пользователю контроль: в настройках можно отключить фоновую синхронизацию, push-уведомления, фоновую загрузку для конкретного сайта.
Эти ограничения являются частью модели доверия веб-платформы. Они позволяют разработчикам строить мощные приложения, не жертвуя конфиденциальностью и контролем со стороны пользователя.
Стратегии управления данными и пользовательский опыт в автономных приложениях
Возвратный кэш (Stale-While-Revalidate)
Stale-While-Revalidate (SWR) — HTTP-директива (Cache-Control: stale-while-revalidate=<delta-seconds>) и архитектурный паттерн, впервые популяризированный в библиотеке swr и адаптированный для Service Worker’ов.
Суть паттерна:
Показать устаревшие, но уже имеющиеся данные немедленно, параллельно инициировав фоновую проверку их актуальности.
Это радикально отличается от классических подходов:
- Cache-First — возвращает кэш, но не обновляет его до следующего запроса (риск показа сильно устаревших данных).
- Network-First — ждёт ответа от сервера, блокируя интерфейс при сетевых задержках или отсутствии связи.
- Network-Only — игнорирует кэш полностью.
SWR обеспечивает немедленный отклик интерфейса при первом открытии (например, списка новостей), а через доли секунды — автоматическое обновление, если сервер вернул более свежие данные. При этом, если сеть недоступна, пользователь всё равно видит содержимое, а не пустой экран с индикатором загрузки.
В Service Worker’е паттерн реализуется следующим образом:
- При получении
fetch-события проверяется наличие ресурса в кэше. - Если есть — немедленно возвращается ответ из кэша (
event.respondWith(cachedResponse)). - Параллельно, в фоне (без ожидания через
event.waitUntil()), запускается запрос к серверу. - При получении свежего ответа кэш обновляется; если данные изменились — посылается сообщение в открытые вкладки (
clients.claim()+postMessage()), чтобы интерфейс обновился без перезагрузки.
SWR особенно эффективен для:
- часто читаемых, редко изменяемых ресурсов (статические страницы, справочники, профили);
- данных, где допустима кратковременная неактуальность (новостные ленты, рейтинги);
- инициализации UI при первом запуске приложения.
Ограничение: не подходит для операций, требующих строгой согласованности (например, проверка баланса перед оплатой). В таких случаях применяется гибридный подход: SWR для чтения, Network-First или Network-Only для записи.
Классификация стратегий кэширования
Помимо SWR, в автономных приложениях применяются следующие фундаментальные стратегии (в терминах Workbox, но применимые и вручную):
-
Cache-Only — возвращать только из кэша. Используется для критически важных статических ресурсов (HTML-оболочка, иконки, скрипты), гарантируя доступность приложения даже при полном отсутствии сети. Рискован для динамических данных.
-
Network-Only — игнорировать кэш, всегда обращаться к серверу. Применяется для операций, где актуальность обязательна (аутентификация, финансовые операции).
-
Cache-First (с fallback) — сначала кэш, при отсутствии — сеть. Основной паттерн для статики и медленно меняющихся данных. Часто дополняется фоновым обновлением кэша после ответа.
-
Network-First (с fallback) — сначала сеть, при ошибке — кэш. Подходит для динамических данных, где важна свежесть, но допустимо показать устаревшую версию при сбое (например, лента сообщений в чате).
-
Stale-While-Revalidate — как описано выше: отдача кэша + фоновый запрос.
Выбор стратегии зависит от семантики ресурса, а не от его типа. Один и тот же эндпоинт /api/user может обрабатываться по SWR при открытии профиля (пользователь увидит своё имя мгновенно), но по Network-First при попытке смены email (требуется свежее состояние сервера для валидации).
Управление состоянием
Автономность требует смены фокуса: вместо хранения данных — хранение операций.
Классическая онлайн-модель:
UI → отправка запроса → обновление состояния после ответа.
Автономная модель:
UI → запись операции в локальную очередь → оптимистичное обновление UI → фоновая отправка операции → подтверждение/откат по результату.
Каждая операция должна содержать:
- Уникальный идентификатор (UUID), чтобы избежать дублирования при повторных отправках;
- Ссылку на сущность (например,
postId: 'abc-123'); - Тип действия (
create,update,delete,patch); - Полезную нагрузку (новые значения полей, delta-изменения);
- Метаданные: временная метка, версия сущности, идентификатор сессии.
Пример структуры операции:
{
"id": "op-9f3a2b1c",
"entity": "comment",
"entityId": "cmt-550e8400",
"action": "create",
"payload": {
"text": "Отличная статья!",
"authorId": "usr-abc",
"postId": "post-xyz"
},
"meta": {
"timestamp": 1732194800000,
"version": 1,
"sessionId": "sess-7d8f"
}
}
Очередь операций хранится в IndexedDB (обычно в отдельной таблице pendingOperations). Service Worker при событии sync извлекает операции, отправляет их на сервер в пакетах (для снижения количества запросов), и по получении подтверждения удаляет их из очереди. При ошибке — сохраняет с новой меткой повтора или помечает как конфликтную.
Такой подход обеспечивает:
- Устойчивость к перезапуску — операции не теряются при закрытии вкладки;
- Идемпотентность — сервер может обрабатывать одну операцию многократно без последствий;
- Трассируемость — лог операций позволяет диагностировать сбои синхронизации.
Разрешение конфликтов
Конфликт возникает, когда локальное и удалённое состояния одной сущности расходятся в результате независимых изменений. Пример: пользователь редактирует пост в офлайне, а в это время модератор удаляет его на сервере.
Автономное приложение обязано иметь стратегию разрешения конфликтов. Она может быть:
- Автоматической (по правилам);
- Полуавтоматической (с участием пользователя);
- Ручной (только через интерфейс разрешения).
Автоматические стратегии
- Победа сервера — локальные изменения отменяются. Подходит для систем, где сервер — единственный источник истины (например, банковские операции).
- Победа клиента — серверные изменения перезаписываются. Опасно, но допустимо для персональных черновиков.
- Слияние по полям — для каждого поля выбирается значение по правилу: «самое новое», «не null», «по приоритету источника». Требует метаданных изменения (кто, когда, какое поле).
- Векторные часы / CRDT — распределённые структуры данных, гарантирующие конвергенцию без центрального координатора. Применяются в продвинутых системах (например, совместные редакторы), но сложны в реализации.
UX при конфликте
Интерфейс не должен «молчать». При обнаружении конфликта:
- Пользователь получает уведомление: «Обнаружено расхождение. Ваша версия и серверная версия отличаются»;
- Предлагается выбор: «Оставить мою», «Принять серверную», «Объединить вручную»;
- В случае ручного объединения — показываются обе версии (например, в split-view), и пользователь выбирает фрагменты.
UX-паттерны автономной работы
Техническая реализация бессмысленна без продуманного взаимодействия с пользователем. Ключевые принципы:
-
Прозрачность состояния сети
Не скрывать факт офлайна. Индикатор (например, иконка с перечёркнутым облаком) должен быть видим, но не агрессивен. Лучше — интегрировать в контекст: «Сохранено локально. Отправится при подключении» рядом с сообщением. -
Оптимистичные обновления
Реакция на действия должна быть мгновенной: сообщение появляется в чате, задача — в списке, черновик — в редакторе. Отложенный характер синхронизации не должен ощущаться как задержка. -
Прогресс, а не спиннеры
При фоновой синхронизации или загрузке — показывать прогресс: «3 из 5 сообщений отправлено», «Загружено 45 %». Это снижает тревожность. -
Управляемость
Пользователь должен иметь возможность:- вручную запустить синхронизацию;
- отменить ожидающую операцию;
- очистить локальные черновики;
- переключить режим кэширования (например, «только по Wi-Fi»).
-
Обратная связь по результату
После завершения фоновой задачи — уведомление не «Синхронизация выполнена», а «Сообщения доставлены», «Документ сохранён на сервере». Конкретика важна. -
Гарантии, а не обещания
Избегать формулировок вроде «Данные будут отправлены позже». Лучше: «Данные сохранены локально и отправятся автоматически при восстановлении связи». Акцент на факте (сохранено), а не на будущем действии.
Тестирование автономности
Проверка работы без интернета не сводится к отключению Wi-Fi в DevTools. Необходимо моделировать:
- Частичную доступность — DNS работает, TCP — нет; HTTP 200 приходит, но тело обрезано; сервер возвращает 5xx.
- Переходы между режимами — онлайн → офлайн во время отправки; офлайн → онлайн при открытой вкладке.
- Закрытие приложения в момент синхронизации — корректно ли восстанавливается очередь?
- Долгосрочное хранение — данные, созданные месяц назад, корректно синхронизируются сегодня?
- Конфликты версий — имитация одновременного редактирования с другого устройства.
Инструменты:
- DevTools → Application → Service Workers (вручную остановить/активировать SW);
- Network Throttling («Offline», «Slow 3G»);
- Плагины для тестирования IndexedDB (например,
idb-keyvalс логированием); - E2E-тесты с принудительным отключением сети между шагами (Cypress, Playwright).