
Привет! Меня зовут Алексей Егоров. Сегодня поговорим про офлайн-режим в PWA и WebView, без лишней теории и с прицелом на мобильные приложения. В разработке я уже больше трёх лет, большую часть этого времени посвятил WebView и мобильным решениям. В этом материале мы разберём, как работают PWA и WebView в офлайн-режиме, посмотрим на инструменты, которые помогают реализовать такую функциональность, и в завершении я поделюсь примером: какую технологию мы выбрали для календаря и как внедрили офлайн-режим на практике.
Зачем разрабатывать мобильные приложения, и тем более в офлайне?
Зачем вообще работать с мобильными приложениями? Ответ очевиден: там огромная аудитория, а значит, это выгодно бизнесу. Но зачем внедрять офлайн-режим? Если проект развивается, то важно улучшать UX, обеспечивать плавность работы. Оффлайн позволяет минимизировать зависимость от сети, загружая данные локально хотя бы при первом запуске. А ещё это ускоряет загрузку: берём статику из локального хранилища, а не запрашиваем каждый раз из сети.
PWA или WebView

Разработанный нами календарь Mail.ru — это полноценная реализация календаря с разными версиями: десктопной, мобильной и с различными виджетами.
Когда перед разработчиками встала дилемма — использовать PWA или WebView, они начали погружаться в детали. PWA — это API браузера, позволяющий веб-приложению выглядеть и работать как нативное, включая поддержку оффлайн-страниц. Например, можно заменить оффлайн-страницу с динозавром в Chrome на кастомную. Установка PWA тоже проста: нажимаем «Install» в браузере, и приложение готово к использованию.
WebView же — встроенный браузер внутри приложения. Работает он в нативном контейнере на iOS и Android по умолчанию. Главное, чтобы WebView мог взаимодействовать с нативным кодом, и для этого используется Bridge: своего рода EventBus на уровне JavaScript. С его помощью мобильное приложение выполняет код и подписывается на события.
Теперь к сути: разберём, какие технологии обеспечивают оффлайн-режим.
PWA

В PWA ключевую роль играют сервис-воркеры. Формально, PWA может существовать и без них, но на практике так не делают. Помимо сервис-воркеров используют Cache API и другие локальные хранилища.
Сервис-воркер — это посредник между PWA и сетью. Он отвечает за кеширование статических ресурсов, обработку уведомлений и обновление контента в фоне. Данные он сохраняет в Cache API, представляющем собой хранилище типа «ключ-значение». Например, в DevTools можно увидеть, как URL изображения сопоставляется с загруженным файлом (пусть даже красной пандой!).


Как реализовать PWA? Довольно просто: достаточно настроить сервис-воркер, манифест — и базовая версия готова.
А что с UX (User Experience)? Возможности PWA ограничены, но постоянно расширяются. Актуальные возможности можно посмотреть по ссылке.
Кроме того, установка происходит через браузер, а не через магазин приложений, что для привычного юзера может выглядеть неудобно.

Производительность? Высокая, поскольку используются браузерные технологии.
Перейдём теперь к WebView и поговорим про его оффлайн.
WebView
В WebView есть всё, что предлагает PWA, но дополнительно можно использовать стандартное кеширование и хранение данных внутри приложения через Bridge.

Что такое стандартное кеширование? Это холодный запуск: данные загружаются из сети, кеш инициализируется, а при следующем запуске приложение достаёт данные из кеша. В Android-коде это просто: включаем AppCacheEnabled
, задаем режим кеширования и путь хранения, и система всё делает сама.

Хранение на стороне приложения устроено иначе: WebView загружает данные не из CDN, а из заранее встроенного zip-архива. Этот архив включён прямо в установочный пакет, так что, скачивая приложение из App Store или Google Play, пользователь получает все ресурсы сразу, без дозагрузок из сети.
Но внедрение WebView требует больше усилий. Если PWA можно запустить, просто сверстав фронтенд, то здесь не обойтись без помощи мобильных разработчиков: понадобится взаимодействие с нативным кодом iOS и Android.
С другой стороны, WebView даёт заметное преимущество по UX. Он интегрируется с Native, а значит, открывает доступ ко всем системным возможностям платформы, которых PWA лишена.
Установка — классическая: пользователь находит приложение в App Store или Google Play, скачивает его и получает привычный нативный опыт.
Производительность? Высокая. Все данные уже хранятся локально, загружаются мгновенно и не зависят от состояния сети, особенно когда речь идёт о статическом контенте.
Что же мы выбрали для календаря? Конечно, WebView. У нас уже был суперап и готовый мобильный контейнер, так что решение выглядело логичным. Вопрос «может, лучше PWA?» даже не поднимался.
А теперь давайте посмотрим, как именно мы это реализовали на практике, какие выводы сделали и какие фичи запустили.
И как получилось?

Что мы сделали для оффлайна? Базовое — кеширование запросов и хранение бандла внутри приложения. Но здесь есть нюанс: обновление бандла привязано к релизу приложения. Напомню, как это работает:
Нужно добавить бандл в репозиторий мобильного приложения.
Затем выпустить новую версию приложения.
После этого начинается раскатка в App Store или Google Play (процесс не быстрый).
А ещё релиз могут отклонить, что увеличивает TTM.

И тогда мы подумали: а почему бы не обновлять бандлы динамически?
Как это работает:
Мы нажимаем Deploy Prod и выкатываем в CDN версию 3.3.
Пользователь пока работает на версии 3.2.
Мобильный контейнер запрашивает CDN: «Есть что-то новое?»
CDN отвечает: «Да, последняя версия — 3.3».
При следующем запуске приложение автоматически использует 3.3.

На схеме выше показан весь этот процесс: мобильный контейнер обращается к CDN, получает свежий бандл и загружает его при следующем запуске. Это позволяет обновлять фронтенд без привязки к релизному циклу приложения.
Что нам это дало? Мы, наконец, избавились от сложности обновлений. Теперь у нас есть отдельные релизы, независимые от приложения и его циклов. Если раньше фронтенд приходилось обновлять вместе с релизом мобильного приложения, то теперь всё гораздо проще: можно выкатывать изменения быстро, гибко и без привязки к релизному процессу App Store.

Самое важное — теперь можно откатить точечно. Например, у нас есть два релиза — веб и мобилка. Мы выпускаем новую версию 1.new, везде её раскатываем, но вдруг видим, что в мобильном приложении что-то пошло не так. Раньше пришлось бы откатывать обе платформы, а теперь можно просто переключить мобильный контейнер на старый бандл, спокойно исправить ошибку и накатить новую версию без потрясений.

Тут же есть ещё один момент — работа с локальными файлами. Раньше открытие календаря работало так: мобильный контейнер говорит: «Открой календарь». Приложение через три секунды отвечает: «Я запустилось». Потом по Bridge идёт следующий запрос: «Мне нужен вид “день”». Ещё секунда, и календарь, наконец, открывает нужный экран. И в сумме на всё это уходило, предположим, пять секунд.

Это как если бы вам в чате просто написали: «Привет», и молчали, пока вы не спросите: «А что случилось?».

Как мы это починили? Использовали довольно логичное решение: добавить кеш-роутинг file:///android_asset/index.html + #/week/view/today. Теперь мобильный контейнер сразу говорит: «Открой календарь и сразу вид “день”». Запуск происходит за, предположим, три секунды, но теперь без дополнительных запросов, так что экран открывается моментально. Было 5 секунд, а стало 3. Пусть не космическая разница, но оптимизация, которая заметна.

Теперь про WebView и возможности для UX. WebView предлагает больше инструментов для кастомизации интерфейса. Например, мы внедрили Remote Layout — систему, которая позволяет на лету конфигурировать нативное меню через фронтенд. Это не библиотека, просто удобное название.

Как это работает? Через Bridge мобилка передаёт интерфейсные настройки. Мы просто отправляем JSON с конфигурацией, и всё меняется в реальном времени. Теперь мобильные разработчики заранее реализуют виды, а фронтендеры сами управляют их отображением.
И ещё одно крутое улучшение — перехват запросов. Теперь мобильное приложение может само отдавать нужные данные, подменяя ответы сервера. Например, мы запрашиваем JS-файл, а приложение просто перехватывает запрос и сразу отдаёт их без похода в сеть.

И что это нам дало? Во-первых, на старте мобильные разработчики активно включились в процесс: внедряли WebView, подгоняли контейнер, настраивали нативные интерфейсы. Но потом… всё перешло в расслабленный режим. Потому что фронтендеры взяли конфигурацию в свои руки. Всё, что раньше требовало участия мобильной команды, теперь контролируется с фронтенда — экономия ресурсов налицо.
WebView не просто упрощает разработку, но и даёт гибкость. Он поддерживает DeepLink и UniversalLink, так что привычные сценарии остаются: раньше пользователь набирал в браузере calendar.mail.ru
, а теперь приложение само перехватывает этот URL и сразу открывает нужный экран внутри себя.
Недостатки тоже есть. Мы избавились от привязки к релизному циклу мобильного приложения, но появился свой релизный цикл бандлов. И да, WebView тесно завязан на натив: если однажды решите сменить контейнер, то придётся выковыривать интерфейсы и собирать универсальную библиотеку.
И ещё одно. Хотите или нет, но вам всё равно придётся немножко уметь отлаживать на реальных устройствах, понимать, как устроены платформы изнутри и почему они ведут себя именно так.
Что бы я хотел знать два года назад?
Вопрос про хранение данных преследовал меня, наверное, года два. Сначала всё казалось элементарным: есть данные — положил куда-нибудь, и всё. Но потом начали вылезать нюансы. Потом ещё. И в какой‑то момент всё перевернулось с ног на голову.
Сначала я думаю: ладно, Local Storage. Оно же синхронное, удобное… но слишком маленькое. Окей, тогда пусть мобильное приложение само заботится о файлах: складывает их в свою файловую систему, а я буду только дёргать. Логично? Да. Но тут оказалось, что мобильные разработчики должны писать отдельную систему хранения, а главное — никто из них не горел желанием этим заниматься. И вот тогда появилась идея с IndexedDB.
На первый взгляд, идеальный вариант. Но нюансов оказалось куда больше, чем я ожидал. Local Storage отпало сразу: в нём и кеш нельзя хранить, и большие данные не прокинешь. Хранение на мобилке мы пробовали. И казалось, что решение верное: приложение забирает и возвращает данные — живём. Но потом начались сюрпризы.
Помню этот момент очень чётко. Мы как раз закатили обновление, всё выглядело отлично… пока ко мне не пришёл начальник с вопросом: «Почему у меня всё крашится?» Мы искали проблему месяц. Перепробовали десятки гипотез. И в итоге выяснилось: если на iOS передавать больше 2 мегабайтов данных туда‑обратно пару раз подряд, то система не выдерживает и падает.
После всех этих экспериментов я окончательно перешёл на IndexedDB. И вот тут встаёт следующий важный вопрос: «А сколько данных она вообще вмещает?»

В Chrome можно занять до 80 % от общего места на устройстве, в Firefox — 50 %, а в Safari — стандартно 1 ГБ, но если попросить разрешение, то можно расширить хранилище на 200 МБ, и так до бесконечности.
Но вот что оказалось настоящей проблемой: кеш в IndexedDB не вечный. Если файл просто лежит в системе, то никто его не удалит. Но вот Chrome решил, что место заканчивается и пора чистить. И угадайте, с чего он начнёт? С наименее используемых данных.
В Firefox такие же правила.
В Safari вообще веселее: если неделю не пользовался приложением, твои данные просто исчезают.
И всё это подводит к следующему вопросу: как сделать кеш постоянным?

Оказывается, можно попросить браузер не удалять данные.

В Navigator Storage есть метод persisted
, который показывает пользователю диалоговое окно. Если он разрешит, то данные останутся навсегда, даже при переполнении. Например, когда вы возвращаетесь после отпуска и ваш телефон забит фотографиями. Это можно протестировать и без поездки — есть инструмент, который заполняет хранилище, чтобы можно было проверить, как система ведёт себя при переполнении.
Но чем это грозит для оффлайна?
Оффлайн может перестать работать. Если данные не записываются в кеш, то приложение начинает работать на старых данных. Если ошибка QuoteExceedError обработана, то просто получаем старый кеш. Если не обработана, то приложение падает.
Как это исправить? Всё просто:
удалять старые кеши, чтобы не захламлять пространство;
следить за свободным местом, чтобы понимать, когда может случиться переполнение;
предупреждать пользователя, если вдруг места не осталось.
Как это отслеживать?

Можно журналировать через Navigator Storage Estimate. Он показывает, сколько места занято, сколько осталось, и какие данные можно ещё записать. Кстати, поддержка очень хорошая, 92 % пользователей смогут этим пользоваться без проблем.
Но самое главное в оффлайне — это общение с пользователем. Приложение должно уведомлять его, когда:
пропал интернет;
медленный интернет;
запрос идёт дольше 10 секунд.
И давайте поговорим о медленном интернете

Знаете эту ситуацию, когда интернет вроде бы есть, но тянет жалкий килобайт в секунду? Тут важно две вещи. Во‑первых, честно сказать пользователю, что всё будет работать медленно. Во‑вторых, если GET‑запрос висит больше 10 секунд, то лучше оборвать его и отдать данные из кеша. Но вот мутации мы ждём до последнего. Потому что если оборвать запрос уже после записи в базу, но до получения статуса 200 OK
, то пользователь увидит: «Не удалось обновить», хотя на самом деле данные уже записались. Этим легко сломать доверие.
И вот ещё любимая история — лифты. Зашли в лифт, связь пропала, и приложение в панике пишет: «Интернет недоступен». Не надо так. Дайте сети шанс: попробуйте перезапросить данные через 3–5 секунд. И только если ответа нет — уведомляйте пользователя.
А ещё правильно подбирайте стратегии кеширования. Например, у нас раньше каждый раз запрашивался email пользователя, хотя он спокойно лежал в кеше. Глупость. Гораздо разумнее хранить локально, а обновлять только по необходимости.
Вот стратегии кеширования, если вдруг подзабыли:
Cache First — отдаёт данные из кеша мгновенно, но может вернуть устаревшее.
Network First — медленнее, зато всегда актуально.
Stale‑While‑Revalidate — гибрид: сначала кеш, потом обновление в фоне.
Cache Only — вообще не ходит в сеть, только локальные данные.
Network Only — всегда из сети, без кеша.
Если вы работаете с WebView и оффлайном, то нужно стать немножко мобильным разработчиком. Понимать, как ведут себя платформы, уметь отлаживать на устройствах, следить за запросами, чувствовать систему. Без этого не получится.
А теперь давайте поговорим о тестировании и отладке.
Что нужно знать про тестирование и отладку?
Это отдельная история. Если вы работаете с WebView, то без хороших инструментов тут никуда.
Во-первых, прокси для отладки. Есть Charles, ProxyMan и другие аналоги. Как это работает?

Представьте, что WebView отправляет запрос GetBalance
, получает ответ от сервера, а вам нужно перезаписать его другим значением и посмотреть, как приложение на это отреагирует.
Но это не всё. Можно, например, смоделировать медленную сеть. Отлавливаем запрос, не пускаем его дальше и смотрим, как приложение ведёт себя в такой ситуации. Это реально полезно, особенно если нужно проверить, как оно справляется с долгими задержками.

Теперь про Android. Тут в помощь Android Studio. Открываем WebView, например, календарь, и начинаем отлаживать. Самый удобный способ — сhrome://inspect/#devices
. Заходим, открываем WebView и получаем полноценные DevTools, как в обычном браузере. Можно посмотреть хранилища, запросы, ошибки — всё, что нужно.

Но есть нюанс. В браузере у нас есть консоль, а в мобильной разработке — logcat. Это уже мобильные логи, и их тоже нужно уметь читать. Иногда мобильного разработчика нет под рукой, а понять, что происходит, всё равно надо.

В iOS есть Xcode — это главный инструмент для отладки. В Safari можно открыть Develop, найти WebView и посмотреть, что там происходит.

Ещё есть, например, Apollo Response. Мы используем его, и в Xcode можно увидеть, как отправляются логи.
Итоги
Если вы задумываетесь о разработке оффлайн‑режима, то первым делом стоит рассмотреть PWA. Это дешевле, проще и в большинстве случаев закрывает базовые потребности пользователей. Но если перед вами стоит задача глубокой интеграции в экосистему супераппа — тогда WebView будет более логичным выбором. Да, это потребует больше ресурсов, зато даст максимум гибкости в UX и доступ к нативным возможностям.
И здесь важно помнить: память на устройстве конечна. На десктопах про это редко думают, а на мобильных идёт вечная война за мегабайты. Ограничивайте размер кеша, чтобы не забивать хранилище телефона, и заранее продумывайте, сколько данных действительно стоит держать локально.
Самое главное — приложение должно общаться с пользователем. Чем понятнее оно объясняет, что происходит, тем выше доверие и лояльность. Никто не хочет в отчаянии писать в поддержку и ждать ответа. Куда приятнее, когда само приложение честно подсказывает, что случилось и как это можно исправить.