Привет, меня зовут Сергей Цибульченко, я fullstack‑разработчик. На хабре уже есть несколько статей про PWA с замахом на исчерпывающее покрытие темы (Разрабатываем PWA. Полная инструкция, PWA вместо приложения: плюсы, минусы и прочие в топе поисковой выдачи Google), но они не раскрывают всех подводных камней данной технологии, я с этим лично столкнулся когда делал пет‑проект по этим статьям. Я не буду рассматривать разработку PWA от начала и до конца, я лишь затрону проблемы, которые возникли у меня в процессе разработки приложения и являются неочевидными и малоосвещенными.
Шаблоны тренировок

Для начала, что такое PWA? Это веб‑страница которая выглядит и ведёт себя как приложение, но при этом имеет технические ограничения веб‑страницы.
Почему я решил написать свою статью о PWA
В апреле 2026 года Google решил что все приложения должны быть подписаны верифицированным аккаунтом Google в консоли разработчика.
Пройти верификацию из РФ не очень уж и просто, а для ряда граждан и юр.лиц невозможно в принципе (упоминание Крыма и новых территорий в документах). Неподписанные приложения просто не будут устанавливаться на Android телефоны с предустановленным Google Play начиная с 2027 года. Статус неподписанных приложений на телефонах Huawei ещё неясен. Из‑за этого PWA становится более актуальной альтернативой особенно в связке с wasm.
Не у всех fullstack‑разработчиков есть возможность и желание приобрести лицензию ios‑разработчика.
Тем не менее, есть потребность сделать кросплатформенный продукт без наличия лицензии, айфона и макбука в обход App Store.
Модерация приложений в RuStore
Некоторые приложения просто не могут пройти модерацию. Например, если вы разрабатываете мессенджер, то с большой долей вероятности на этапе модерации вам откажут. Отдельной болью можно выделить монетизацию для самозанятых и прочие правила и проблемы магазина, а хочется сделать так как надо нам, а не как надо магазинам приложений.
Нет времени учить Kotlin/Flutter и C#
Это хорошо когда можно сделать продукт используя знакомые для веб‑разработчика html/css/js в мобильном приложении.
Крайне слабое представление о PWA у коллег, а тем более, клиентов.
Относительно легко завернуть PWA в Capacitor.
Если в последствии возникнет необходимость (и бюджет) продвигаться через магазины приложений, можно сгенерировать WebView в Capacitor. При условии поддержки offline режима модерация не должна быть проблемой.
Техническое задание на проект
Приложение фитнес‑трекер с поддержкой режима offline, то есть offline‑first PWA.
Приложение должно работать на Android и iPhone, в том числе, на старых моделях.
Функционал на уровне известного трекера Strong, где можно создавать шаблоны тренировок, отмечать подходы, сохранять историю тренировок, автоматически отмечать таймстампы начала и окончания подхода (таймеры отдыха), вести статистику прогресса по упражнениям.
Редактируемая из админки на сервере база из 1200 упражнений с анимированными изображениями.
Синхронизация данных с сервером, когда устройство выходит в онлайн, возможность сгенерировать ссылку на завершенную тренировку и поделиться публичной ссылкой.
Простейшая регистрация по email/password просто чтобы у пользователя был id по которому можно синхронизироваться при переносе данных на другое устройство.
Авторизация

Зачем я делал это приложение. «У самурая нет цели, есть только путь». Это пет‑проект в стол для меня лично и небольшого количества моих знакомых, отработка навыков разработки PWA на случай если всё же понадобится делать что‑то масштабное на заказ. Возможно, приложение когда‑нибудь станет публичным. Тем более, что Strong ушел с рынка РФ и не хочется платить за премиум, уж лучше своим пользоваться.
Выбранный стек
Vue.js (SPA)
bootstrap 5
Dexie.js для работы с indexedDB
Pinia для сохранения состояния при переходе между табами приложения
tanstack/vue‑virtual для виртуальных списков
Laravel/Postgres/Redis для API на бекенде
Я выбрал именно этот стек из‑за того, что я был с ним хорошо знаком (кроме dexie), можете без проблем заменить vue на react и серверную часть на то, чем владеете, тут это не принципиально.
От чего сразу пришлось отказаться
Данные из фитнес‑браслетов. Конкретно к теме PWA это не имеет отношения, это проблемы моего пет‑проекта, возможно, кому‑то информация будет полезна. Изначально мне очень хотелось вытягивать данные по пульсу, SpO2 и стрессу, накладывать их на график упражнений и отдыха. Но я наткнулся на ряд проблем, некоторые из них оказались непреодолимыми:
Нет единого стандарта данных для wearables. Каждый изготовитель реализует это как хочет. Добавляет проблем, но решаемо, в том числе уже готовыми библиотеками.
Особая категория персональных данных. И хотя фитнес‑браслеты и смарт‑часы не являются медицинскими приборами, не могут использоваться в медицине, данные из них всё равно считаются особой категорией ПД, что тянет за собой целый пласт неприятной бюрократии. В теории, решаемо, но не для пет‑проекта одного разработчика. Часть этой проблемы можно обойти не отправляя данные на сервер, а хранить только на устройстве.
Нежелание изготовителей фитнес‑браслетов предоставлять доступы к SDK физическим и юридическим лицам из РФ. Это обойти я уже был не в силах.
Трудности процесса разработки
Подробно я не буду их разбирать, только перечислю:
AI мало знакомы с PWA и с indexedDB. Модели будут часто генерировать код, который не будет работать в PWA, постоянно будут пытаться создавать новые версии баз в indexedDB. Необходимо жестко прописать в скилах и гайдлайнах для AI не создавать новые базы и что мы работаем с offline‑first PWA, а не обычным SPA сайтом. Также надо будет чётко прописать где хранятся токены авторизации пользователя, а где всё остальное, иначе модель запутается между localStorage и indexedDB. У любителей LLM ручных правок Ai‑кода будет на порядок больше, ревьювить придётся более пристально.
Если вы раньше не делали frontend для мобильных телефонов, в вёрстке обязательно учитывайте Safe Area всех вариантов комбинаций браузер + ОС чтобы меню или другие UI‑элементы не наехали на камеру мобильника, стандартный эмулятор телефона в Chrome в этом не поможет. Надо тестировать на реальном телефоне или в Android Studio на виртуальных.
Длинные списки из сотен элементов гарантированно повесят телефон, необходимо использовать виртуальные списки
Brainstorm с Ai. Если владеете английским языком, сразу переходите на английский. Иначе на русском DeepSeek и ChatGPT будут выдавать информацию только с Хабра, а про PWA её тут мало.
PWA требует https или localhost и никак иначе. Придется повозиться с самоподписанными сертификатами, обычный
npm run devтут не справится. Некоторые дистрибутивы Linux даже localhost не хотят принимать, им нужен только https.
Из последнего пункта вы наверно уже догадались, что PWA может быть установлен не только на телефон, но и как десктопное приложение, Windows и Ubuntu отлично с ним работают (при наличии в ОС браузера кроме Firefox), пользователь после установки запускает PWA как обычный ярлык. То есть, можно делать на html/css/js кроссплатформенное mobile‑first приложение с поддержкой offline режима.
А теперь, как и обещал, перечислю подводные камни в разработке PWA.
Первый подводный камень. Где хранить динамические данные для offline‑режима?
Моя цель была разработать именно offline‑first приложение, привязка работоспособности к наличию связи с сервером это компромисс на который я не был готов идти. И тут возникает закономерный вопрос — где хранить динамические данные? Любой backend‑разрботчик недолго думая ответит: «В webSQL браузера, а картинки можно записывать как blob туда же». Я тоже так думал, но как оказалось, webSQL уже давно deprecated и его в браузерах нет.
После изучения различных технологий, я пришел к выводу, что единственный вариант — indexedDB. Варианты вроде wasm + sqlite я не рассматривал. У indexedDB есть как свои плюсы, так и минусы.
Во‑первых, это nosql‑хранилище где можно хранить документы, создавать индексы, проводить поиск и фильтрцию по значениям документов. Во вторых, на чистом js с indexedDB работать очень сложно, к счастью, есть бесплатная библиотека Dexie (да‑да, аналоги Dexie платные), она серьёзно облегчает взаимодействие с базой и даже предоставляет реактивность для Vue.js или React. Можно по API или в фоновых процессах удалять или сохранять данные, браузер реактивно отреагирует на изменения.
Второй подводный камень. Как в PWA добавить 1200 изображений?
В моей базе более тысячи упражнений, у каждого был собственный GIF‑файл и возможность прикрепить видео. Я посмотрел как это реализовано в других фитнес трекерах для Android.
Вариант 1: просто вставить img src="https://адрес_изображения" и оно доступно для просмотра только когда устройство в онлайне.
Вариант 2: изображения добавлены в bundle, являются статическим контентом, скачиваются на телефон вместе с приложением, изменения только при повышении версии приложения.
Первый вариант мне не нравился хотя именно так и поступили разработчики приложения Strong, на которое я ориентировался.
Второй вариант был нерелевантный, я делал PWA, а не Android‑приложение, папки bundle у меня нет. Также, у меня на серверной части упражнения могли как добавляться, так и изменяться, клиент обязан был на это реагировать по API и обновлять локальную базу. То есть, динамический контент, а не статика. Если важность данной проблемы не совсем понятна, представьте, что вы делаете мессенджер и вам надо сохранять большое количество сообщений с пересылаемыми файлами и это должно быть доступно в offline‑режиме для просмотра на обычной веб‑странице. Видео я решил оставить обычным тегом <video> с доступностью только в online‑режиме.
Третий подводный камень. Размер базы данных
1200 GIF‑картинок суммарно легко перевалили за 350Мб (и это не считая данных о всех тренировках пользователя которые в ту же БД записываются) и меня это не устраивало. Изучив варианты, я написал скрипт на Python чтобы их все перегнать в формат avif. Размер базы упал до 37Мб. Браузер при наличии хорошего сигнала wi‑fi скачивал и локально сохранял их все очень быстро в виде blob.
Четвёртый подводный камень. Сброс кеша браузером
После закрытия PWA браузер на своё усмотрение удалял базу из indexedDB, даже 37Мб уже часто вызывали удаление кеша. На этот случай необходимо запрашивать у пользователя разрешение на Persistent Storage. Но полагаться на то, что браузер не прихлопнет БД, всё равно не стоит, необходимо иметь fallback на случай удаления базы и скачивание нужных данных по API с сервера. Это особенно важно для Safari на iPhone, где браузер не сильно уважает мнение как разработчика, так и пользователя, удаление может произойти не сразу, а через пару дней. Также, пользователь может не выдать разрешение на Persistent Storage. Уж лучше в offline‑режиме показать «Необходима синхронизация» чем оставить приложение в нерабочем состоянии.
Пятый подводный камень. Notifications
Если вы хотите реализовать уведомления средствами браузера, учтите что это не очень хорошо работает. Во‑первых, пользователь должен дать согласие на показ уведомлений. Пользователь может этого не сделать. Во‑вторых, будут заметные задержки в доставке сообщений, фоновые процессы PWA имеют невысокий приоритет когда PWA свёрнуто или закрыто. Тем не менее вы можете подключить Firebase к своему PWA, просто учтите, что это работает не так надёжно как уведомления нативных приложений.
Итак, нам понадобятся несколько файлов для минимальной работы PWA. Для начала, нужен файл manifest.json.
{ “name”: “MyApp”, “short_name”: “MA”, “icons”: [ { “src”: “/android-chrome-192x192.png”, “sizes”: “192x192”, “type”: “image/png” }, { “src”: “/android-chrome-512x512.png”, “sizes”: “512x512”, “type”: “image/png” } ], “theme_color”: “#ffffff”, “background_color”: “#ffffff”, “display”: “standalone” }
Ещё потребуется сервис воркер. Минимально рабочий файл sw.js выглядит вот так.
self.addEventListener(‘fetch’, (event) => {event.respondWith(fetch(event.request));});
Потом вы его будете наполнять, а точнее вы будете генерировать этот файл с помощью npm‑команд.
И тут начинается интересное. Сервис воркер не будет постоянно работать в фоновом режиме. Не помещайте туда вызовы API и прочие долгоиграющие важные процессы. ОС телефона может убить его в любой момент, гарантируется работоспособность только если все три условия соблюдены:
телефон не заблокирован
PWA запущен
PWA не свёрнут, приложение видно на экране устройства
Запускайте синхронизацию по API только когда пользователь использует PWA и не из sw.js, приоритет у фоновых процессов свёрнутой веб‑страницы меньше по сравнению с фоновыми процессами нативных приложений. Звуковые уведомления работают ожидаемым образом — если пользователь ещё не взаимодействовал с интерфейсом, автовоспроизведение аудио не будет работать. Если же взаимодействовал, то проблем быть не должно сверх тех, что уже есть у вкладки браузера. В моём пет‑проекте звуковые оповещения используются в таймерах отдыха. С таймерами то же самое — отказ от любых setInterval() в пользу setTimeout().
И последнее. Основная проблема PWA на данный момент как раз таки не в разработке, а в продвижении. В РФ нет магазина где можно выложить PWA или TWA, а среднестатистический пользователь никогда в жизни с ним не сталкивался. Придётся продвигать приложение самостоятельно с инструкциями как его устанавливать, для инди‑проектов конкурировать с магазинами приложений — непосильная задача. Если же вы делаете приложение для сотрудников конкретной организации, тут всё намного проще как для вас, так и для организации — разработка с куда более скромным бюджетом в сравнении с Android+IOS в обход магазинов и правил модерации.
Комментарии (12)

Fragster
05.08.2026 09:26Так как решили с картинками? У меня "с наскока" в оффлайн режиме ничего не работает, и даже если сначала загрузить в онлайне, чтобы оно закэшировалось, при повторном запуске в оффлайне все равно ничего не показывает.

Aurum_Gallente Автор
05.08.2026 09:26У меня в indexedDB упражнения записываются как json-объект. Одним из атрибутов объекта идёт blob изображения. В интерфейсе я этот блоб преобразую обратно в изображение.
indexedDB это key-value значение. Если нужно кешировать только картинки, можно по id хранить их блобы. А потом уже в коде делатьconst imageUrl = URL.createObjectURL(imageBlob);и присваивать этот
imageUrlнужному объекту, потом использовать как:src="obj.imageUrl"
Fragster
05.08.2026 09:26У меня там были динамические пути к ассетам вида:
function getImageUrl(name) { return new URL(`../assets/icons/${name}.svg`, import.meta.url).href }При этом добавлял иконки в сборку через
VitePWA({ includeAssets: ["/favicon.ico", '**/*.{svg,png}'], strategies: "injectManifest", injectManifest: { globPatterns: ['**/*.{svg,png,js,css,html}'], } })],а само кэширование в тупую через workbox
cleanupOutdatedCaches() && precacheAndRoute(self.__WB_MANIFEST).И точно помню, что кэш не работал, при запуске в оффлайн режиме все js отрабатывали, а иконки были битыми. А сейчас проверил - всё супер работает, при этом я даже ничего не пересобирал, как 4 года назад тестово прикрутил pwa, так и оставил.

Aurum_Gallente Автор
05.08.2026 09:26Если этих иконок будет на 30+ мегабайт, результат будет немного другим. Для малого количества данных (по объему) это сработает. Я же описывал вариант 37-350Мб.

Fragster
05.08.2026 09:26Отдельно вместо бутстрапа рекомендую tailwind, да.

Aurum_Gallente Автор
05.08.2026 09:26Вообще да, согласен. Но лично я с вёрсткой не очень плотно дружу. Я хоть и fullstack, но с перекосом в backend. Для PWA я выбрал bootstrap, так проще было лично мне сложные формы заполнения подходов делать, а уже при вёрстке лендинга (PWA на поддомене, api + лендинг + ресет пароля на основном) и формы ресета я делал с tailwind. Если б это был коммерческий проект и наличие хотя бы ещё одного человека в команде, то делалось бы на tailwind.

popovichihor
05.08.2026 09:26Автор реально хорошо показал обратную сторону PWA, не только возможности, но и проблемы, с которыми сталкиваешься в разработке. Особенно полезно про offline, кеш и indexedDB, потому что такие нюансы редко учитывают заранее. Для небольших проектов это действительно может быть отличным решением.

Aurum_Gallente Автор
05.08.2026 09:26Там ещё очень много всего - Apple разрешают запускать PWA только на движке Safari даже если основным браузером в системе был установлен другой. В Safari не работает функционал установки по кнопке и многое "не как у людей".
От wasm я отказался как раз таки из-за "полезных" советов ИИ на этапе планирования, пара нейронок написали что при использовании wasm в PWA нельзя будет загружать изображения и видео по src="https://..." и делать кросс-доменные запросы к api. Когда я перешел на английский язык, выяснилось, что это неправда, но было уже поздно.
Изначально я хотел мимикрировать под нативные приложения и наследовать тему и стили системы, но разные браузеры это делали немного по-разному, поэтому я полностью отказался от использования стилей ОС.
При установке на десктоп уведомления через гугловский Firebase разоботают, но отображаются в системе очень по-разному в зависимости от ОС.
PWA работает немного по-разному до установки и после установки. Его можно использовать просто перейдя по ссылке на url приложения и использовать в браузере. А вот после установки на рабочий стол прав у PWA чуть больше, но на разных телефоах разница своя.

DiRo
05.08.2026 09:26В ветке про предзагрузку ассетов вы верно отвечаете, что на 350 МБ это не работает. Но эти 350 МБ — гифки, а не сам справочник: названия, теги и группы мышц весят несоизмеримо меньше, и их имеет смысл считать отдельным хранилищем. У нас справочник сопоставимого размера — 32 772 записи — лежит не в базе, а как 1387 статических файлов примерно по 1,4 КБ, разложенных по префиксу; сервис-воркер держит их в Cache Storage, поиск и фильтрация идут в браузере. Выигрыш не в объёме, а в обновлении: изменившийся кусок справочника — это условный запрос по ETag на полтора килобайта, без версии базы и без миграции. Заодно уходит ровно та боль, о которой вы пишете в разделе про AI: пересоздавать версию базы модели уже негде, версионируется только пользовательская часть. Медиа при этом всё равно остаются блобами в IndexedDB, их так и так качать. Не пробовали разнести справочник и картинки по разным хранилищам?

Aurum_Gallente Автор
05.08.2026 09:26Пока что нет. Но если я решу что каждое упражнение может иметь более одного изображения, то придётся рефакторить и смотреть где можно сделать попроще.
frog
Между прочим, напрасно. Отлично работает (в связке с OPFS), в том числе на старых телефонах. Есть мелкие нюансы, но они просто несравнимы с плясками вокруг IndexedDB.
Aurum_Gallente Автор
Я пробовал решения с sqlite, но уже когда завернул PWA в Capacitor. Нет реактивности для vue.js, это было заметно при работе с фильтрами. Пользователю надо было в двух интерфейсах быстро искать и фильтровать упражнения по тегам, названию, группам мускулатуры, быстро подгружать изображения найденных упражнений. Если я бы я переделывал с нуля это приложение, то возможно рассмотрел бы такой вариант. В моём случае indexedDB хорошо подходил тем, что можно было 1200 упраженений вместе с файлами записать как объекты, добавить индексы и быстрый поиск по нужным ключам. Ранее я не работал с wasm (хотя сейчас активно его штурмую), не знаю всех особенностей в каждом из мобильных браузеров, поэтому не захотел тащить это в проект. Поплясать с idexedDB пришлось, не спорю, но пляски мне показались меньшим злом в сравнении с рисками wasm о котором я знаю слишком мало пока что.