Офлайн-режим в мобильном приложении: когда он нужен и сколько стоит
Офлайн-режим удваивает сложность работы с данными. Разбираем, каким приложениям он действительно нужен и как выбрать стратегию синхронизации.

«Пусть работает и без интернета» звучит как небольшое дополнение к требованиям, а на деле меняет всю работу с данными. Приложение перестаёт быть тонким клиентом к серверу и становится второй копией базы, у которой своя история изменений. Отсюда конфликты, очереди операций и вопросы, на которые в онлайн-приложении отвечать не приходилось.
Это не значит, что офлайн не нужен. Значит, что решение стоит принимать осознанно, а не по инерции.
Четыре уровня офлайн-поддержки
| Уровень | Что умеет | Надбавка к смете |
|---|---|---|
| Понятная ошибка | Показывает состояние сети и кнопку повтора | до 5% |
| Кэш на чтение | Открывает ранее загруженные экраны | 10–15% |
| Очередь операций | Копит действия и отправляет при сети | 25–40% |
| Полная репликация | Работает автономно неограниченно долго | от 60% |
Большинству приложений достаточно второго уровня, а разговор обычно начинается с четвёртого. Кэш на чтение закрывает типовой сценарий: человек зашёл в метро, приложение показало последние данные вместо пустого экрана. Очередь операций нужна там, где действие нельзя отложить: акт выполненных работ у монтажника, отметка о приёмке на складе, замер у инженера в подвале.
Очередь операций
Базовая идея: локально применяем изменение сразу, а в очередь кладём описание операции. Когда сеть появилась, отправляем операции по порядку. Важная деталь — операции должны быть идемпотентными: сервер обязан узнать повторную отправку и не создать вторую запись.
type PendingOp = {
id: string // uuid, генерируется на устройстве
entity: "inspection" | "photo" | "comment"
action: "create" | "update" | "delete"
payload: unknown
createdAt: number
attempts: number
}
export async function flushQueue() {
const ops = await db.pending.orderBy("createdAt").toArray()
for (const op of ops) {
try {
// Сервер проверяет op.id и при повторе возвращает прежний результат
await api.apply(op)
await db.pending.delete(op.id)
} catch (error) {
if (isConflict(error)) {
await markForReview(op) // решает пользователь
await db.pending.delete(op.id)
continue
}
// Сетевые ошибки — оставляем в очереди с задержкой
await db.pending.update(op.id, { attempts: op.attempts + 1 })
break
}
}
}Конфликты и кто их решает
Конфликт возникает, когда одну запись поменяли и на устройстве, и на сервере. Универсального правильного решения нет — есть выбор, который надо сделать явно и объяснить пользователю.
| Стратегия | Когда подходит | Риск |
|---|---|---|
| Последняя запись побеждает | Личные заметки, черновики | Тихая потеря чужих правок |
| Сервер главный | Справочники, цены, каталоги | Потеря работы, сделанной офлайн |
| Устройство главное | Замеры и акты с объекта | Затирание правок из офиса |
| Показать пользователю | Документы, где важна каждая правка | Сложнее интерфейс, нужен экран сравнения |
| Слияние по полям | Формы, где правки редко пересекаются | Сложная логика, трудно тестировать |
На практике в одном приложении сочетаются несколько стратегий: справочники тянутся с сервера без вопросов, а данные, введённые в поле, считаются приоритетными и при расхождении выносятся на подтверждение.
Что усложняется вместе с офлайном
- Миграции локальной базы: у пользователя может стоять версия полугодовой давности с непустой очередью
- Тестирование: нужны сценарии с обрывом сети посередине операции и с расхождением часов на устройстве
- Хранение файлов: фотографии с объекта занимают память и требуют отдельной очереди загрузки
- Безопасность: локальная база с данными клиентов должна шифроваться, а при выходе — стираться
- Поддержка: разбор жалобы «пропали данные» требует журнала операций с устройства
Последний пункт стоит заложить сразу: без журнала операций и возможности его выгрузить любая история о потерянных данных превращается в спор без доказательств.
Частые вопросы
- Насколько офлайн-режим удорожает приложение
- Кэш на чтение — плюс 10–15% к смете, очередь операций с разрешением конфликтов — 25–40%, полная автономная работа — от 60%. Дороже становится и поддержка: тестировать приходится больше состояний.
- Можно ли добавить офлайн позже
- Можно, но это почти всегда переработка слоя данных, а не добавление функции. Если автономная работа вероятна в перспективе года, дешевле сразу построить работу с данными через локальное хранилище, даже без очереди операций.
- Какие библиотеки вы используете
- Для React Native — WatermelonDB или SQLite с собственным слоем синхронизации, для простых случаев MMKV и кэш запросов TanStack Query. Выбор зависит от объёма данных и того, нужна ли реляционная модель на устройстве.
Похожая задача в вашем проекте?
Разберём вашу ситуацию и пришлём оценку по этапам — без обязательств и общих слов.
Обсудить задачу

