Когда веб-приложение начинает хорошо работать на мобильных устройствах, почти неизбежно возникает следующий вопрос:
Нужно ли делать отдельное мобильное приложение?
На первый взгляд есть два противоположных пути.
Первый:
Web
↓
PWAОставить продукт веб-приложением, добавить manifest, Service Worker, установку на домашний экран и offline-возможности.
Второй:
Web
↓
Native shell
↓
Android / iOSУпаковать веб-приложение в мобильную оболочку и распространять через мобильные платформы.
Часто эти варианты обсуждают как взаимоисключающие:
Или PWA, или мобильное приложение.
В одном из наших проектов мы пришли к другой архитектуре.
В приложении одновременно существуют:
обычная веб-версия
+
PWA
+
Service Worker
+
Capacitor 8
+
Android
+
iOSПри этом основная бизнес-логика интерфейса не переписывается три раза.
Получается одна web-first система, у которой есть несколько способов доставки пользователю:
Web application
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Browser PWA Capacitor
│
┌─────┴─────┐
▼ ▼
Android iOSТакой подход оказался интереснее простой дискуссии «что лучше».
Потому что PWA и Capacitor решают разные задачи.
Разберём, зачем поддерживать оба варианта, где действительно можно использовать общий код, а где различия платформ всё равно приходится учитывать.
Сначала определимся: что даёт PWA
Progressive Web App остаётся веб-приложением.
Пользователь может открыть его обычной ссылкой:
https://example.ruи сразу начать работу.
Никакого магазина приложений.
Никакой предварительной установки.
Но при наличии Web App Manifest поддерживающий браузер может предложить установить приложение на устройство. После установки оно получает иконку и может запускаться в более самостоятельном режиме, похожем на обычное приложение. Service Worker часто используется для offline-сценариев, кеширования и фоновых возможностей.
Получаем очень сильное свойство:
ссылка
↓
открыли
↓
работаемА уже потом:
понравилось приложение
↓
установилиДля продукта это означает минимальный порог входа
Представим сервис планирования или личный кабинет.
Пользователю отправляют ссылку:
example.ruОн не должен сначала:
найти приложение;
открыть магазин;
скачать;
дождаться установки;
открыть;Можно сразу показать продукт.
Это особенно полезно для:
SaaS;
CRM;
B2B-кабинетов;
сервисов планирования;
внутренних приложений;
контентных платформ.PWA сохраняет главное преимущество Web — мгновенное распространение версии
Мы обновляем сервер.
Пользователь при следующем корректном обновлении приложения получает новую web-версию.
Не требуется отдельно публиковать каждое изменение через мобильный store.
Хотя Service Worker добавляет собственную задачу — управление кешем и жизненным циклом обновления.
Если сделать его неправильно, можно получить неприятную ситуацию:
Backend:
version 3.5
Server frontend:
version 3.5
User Service Worker cache:
version 3.4То есть сервер уже новый, а пользователь продолжает работать со старым JavaScript.
Поэтому PWA — это не просто manifest и кнопка «Установить»
Production PWA требует подумать о:
cache version;
Service Worker lifecycle;
offline fallback;
update strategy;
старых cache;
network failures.Сам Service Worker становится отдельным инфраструктурным компонентом frontend.
Что тогда даёт Capacitor
Capacitor решает другую задачу.
Это native runtime для web-first приложений: существующий HTML/CSS/JavaScript интерфейс можно запускать внутри нативного Android/iOS приложения и при необходимости обращаться к native API через плагины. В актуальной документации Capacitor 8 официально поддерживаются Android, iOS и Web как целевые платформы.
Условно:
Web code
↓
Capacitor
↓
Native WebView
↓
Android / iOSНо важное слово здесь — не WebView.
А native bridge.
JavaScript получает доступ к возможностям устройства
Например приложению могут понадобиться:
камера;
файловая система;
геолокация;
нативные уведомления;
haptics;
status bar;
share sheet;
нативный lifecycle.Capacitor предоставляет плагинный слой между web-кодом и платформой, а при необходимости можно написать собственный native plugin. Официальная документация прямо позиционирует Capacitor как web-first runtime с доступом к нативным SDK.
Значит ли это, что PWA больше не нужна?
Нет.
Это первое важное архитектурное решение.
Capacitor добавляет ещё один канал доставки.
Он не обязан заменять web.
Можно сохранить:
Web / PWAдля пользователей, которым удобно открыть приложение по ссылке.
И одновременно:
Android / iOSдля пользователей, которым нужен привычный установленный mobile app.
Получается один продукт с разными входами
Например:
Desktop user
↓
Browser
↓
Web
Android browser user
↓
PWA
Android app user
↓
Capacitor Android
iPhone browser user
↓
Web / installed web app
iPhone app user
↓
Capacitor iOSНа backend все они могут обращаться к одному API.
Архитектура выглядит примерно так
Backend API
│
PostgreSQL / etc.
│
┌───────────┼───────────┐
│ │
▼ ▼
Browser/PWA Native apps
│ │
│ Capacitor 8
│ / \
▼ ▼ ▼
Service Worker Android iOSBackend не должен иметь три совершенно разных бизнес-модели только потому, что появилось несколько клиентов.
Но «одна кодовая база» нужно понимать правильно
Очень легко представить:
Написали сайт — нажали кнопку — получили Android и iOS.
В реальности всё немного сложнее.
Общий слой действительно может быть очень большим:
UI;
domain logic;
API client;
формы;
валидация;
состояние приложения;
большая часть offline logic.Но Capacitor также создаёт полноценные нативные проекты:
android/
ios/которые имеют:
permissions;
native configuration;
signing;
store metadata;
platform resources.То есть правильнее говорить:
общий web-core + platform-specific adapters и native projects.
Мы стараемся не писать так
if (android) {
// 400 строк
}
if (ios) {
// ещё 350
}
if (web) {
// третий вариант
}Если платформенные проверки начинают проникать во всю бизнес-логику, общий код быстро превращается в смесь трёх приложений.
Лучше создать platform layer
Например:
src/
├── domain/
├── ui/
├── api/
│
└── platform/
├── notifications.js
├── files.js
├── share.js
├── haptics.js
└── lifecycle.jsБизнес-код спрашивает:
platform.notifications.requestPermission()а не:
Если iOS — делай одно, если Android — второе, если PWA — третье.
Например интерфейс уведомлений
export async function
requestNotifications() {
// platform-specific implementation
}Web adapter может использовать web-возможности.
Native adapter — Capacitor plugin.
UI не обязан знать техническую реализацию.
Capacitor умеет определять текущую платформу
В API есть разделение между:
web
ios
androidа также проверка, запущено ли приложение как native Capacitor app или как Web/PWA. Это позволяет локализовать platform branching в небольшом количестве мест вместо распространения if по всему приложению.
Концептуально:
if (Capacitor.isNativePlatform()) {
// native capability
} else {
// browser / PWA
}Но даже такую проверку лучше прятать внутри platform adapter, если она начинает повторяться.
Почему PWA и Capacitor хорошо сосуществуют
Потому что возможности можно разделить на несколько уровней.
Уровень 1 — универсальный Web
Работает почти везде:
личный кабинет;
дневник;
задачи;
формы;
аналитика;
статьи;
профиль;
настройки.Это основа продукта.
Уровень 2 — PWA enhancements
Например:
installable experience;
manifest;
offline shell;
Service Worker caching;
web push там, где поддерживается;
background web capabilities.Приложение остаётся сайтом, но становится удобнее как установленный web app.
Уровень 3 — Native enhancements
Например:
native notifications;
native filesystem;
device APIs;
нативный share;
native lifecycle;
платформенные permissions.Они доступны через Capacitor.
Это и есть progressive enhancement на уровне продукта
Базовый сценарий:
работает в браузере.Браузер поддерживает PWA:
получаем установку и дополнительные web-возможности.Пользователь использует native app:
добавляем device integration.Но основная бизнес-ценность остаётся одной.
Очень важно не делать native feature обязательным для базовой функции без причины
Представим приложение позволяет добавить фотографию.
На Android/iOS можно использовать:
native Camera plugin.Но Web-версия всё равно может позволить:
<input type="file">То есть:
Native:
камера напрямую
Web:
выбор/загрузка файлаПользователь решает одну задачу разными средствами.
Это лучше, чем отключать функцию полностью
Плохо:
Если не Capacitor
→ функция недоступна.Хорошо:
есть native capability?
→ используем лучший native UX
нет?
→ web fallback.Capability важнее названия платформы
Не всегда нужно спрашивать:
Это Android?Гораздо полезнее:
Доступна ли нужная возможность?Например:
if (
Capacitor.isPluginAvailable('Camera')
) {
// native path
} else {
// web fallback
}Capacitor предоставляет такую проверку plugin availability именно для подобных сценариев.
Теперь самый интересный вопрос — Service Worker
В PWA Service Worker имеет понятную роль:
Browser
↓
Service Worker
↓
Cache / NetworkНапример:
app shell;
offline fallback;
статические assets;
controlled caching.В native build модель уже другая
Capacitor использует собранные web-assets внутри native приложения; конфигурация Capacitor задаёт webDir — каталог с финальными web-файлами, включая index.html, которые используются нативным проектом.
Поэтому нативному приложению не нужно рассчитывать на Service Worker для того, чтобы получить базовый HTML/JS shell:
Native package
↓
bundled web assets
↓
WebViewFrontend уже находится внутри установленного приложения.
Это значит, что offline нужно разделить на два понятия
Очень часто говорят:
Capacitor работает offline, потому что файлы находятся в приложении.
Это только половина истины.
Да, можно открыть локальный application shell.
Но нужны ли пользователю данные?
Например:
список задач;
записи дневника;
проект;
сообщения.Если всё это приходит только из API:
Shell offline ✓
Useful data offline ✗Настоящий offline-first требует отдельной модели данных
Например:
Local state
↓
local persistence
↓
user edits offline
↓
network returns
↓
sync
↓
serverCapacitor сам по себе эту задачу не решает.
PWA Service Worker тоже не решает автоматически.
Он может кешировать network responses и assets, но бизнесовая синхронизация конфликтующих пользовательских данных — отдельная архитектурная задача.
Поэтому мы разделяем
Offline shellи:
Offline business data.Это разные уровни.
Service Worker не должен становиться обязательной частью native logic
Если Service Worker создан для PWA, мы бы не строили критическую Android/iOS-функцию на предположении:
В WebView всё будет работать точно так же, как в Chrome PWA.
Native runtime имеет собственный lifecycle и собственные API.
Критичные native-сценарии лучше реализовывать через явно поддерживаемые механизмы Capacitor.
Это особенно касается push-уведомлений
С точки зрения пользователя функция называется одинаково:
«Уведомить меня»Но техническая доставка в Web/PWA и в native app может быть разной.
Поэтому хороший abstraction выглядит так
NotificationService
│
┌────┴────┐
▼ ▼
Web Native
push pushBackend при этом может работать с общей концепцией:
notification subscriptionsно знать тип endpoint:
web
android
iosНе нужно притворяться, что delivery transport одинаковый
Это важное правило гибридной архитектуры.
Можно унифицировать business intent:
Напомнить пользователю о задаче.Но техническая доставка может быть платформенной.
То же относится к permissions
На Web пользователь видит один UX запроса разрешения.
На Android — другой.
На iOS — третий.
Нативные платформы требуют собственных permission/configuration layers; в iOS, например, определённым native API нужны Usage Descriptions в Info.plist.
Значит permission flow нужно проектировать как пользовательский сценарий.
Не просто:
requestPermission()при первом открытии приложения.
Лучше объяснить пользователю, зачем разрешение нужно
Например:
Хотите получать напоминания
о запланированных задачах?После явного действия:
[Включить уведомления]уже вызывается platform permission.
Это работает и UX-wise, и архитектурно чище.
Следующая важная разница — обновления
У PWA и native shell совершенно разный release lifecycle.
Web/PWA
Условно:
Developer
↓
build
↓
server deploy
↓
Service Worker update
↓
users gradually receive new versionКоманда контролирует deployment напрямую.
Capacitor
Для изменения packaged native приложения обычно требуется:
web build
↓
Capacitor sync
↓
Android/iOS build
↓
sign
↓
mobile release
↓
user installs/updateИ это уже отдельный release channel.
В итоге одновременно могут существовать разные frontend versions
Например:
Web:
4.8
Android:
4.7
iOS:
4.6Backend при этом:
API:
5.1Это один из главных архитектурных вызовов.
Backend должен учитывать version skew
Очень опасно проектировать API так:
Deploy backend 5.1
↓
старый mobile client
перестаёт работатьПользователь может не обновлять приложение неделями.
Поэтому API желательно изменять совместимо
Например было:
{
"name": "Project A"
}Новому клиенту нужно дополнительное поле:
{
"name": "Project A",
"archived": false
}Добавить поле обычно безопаснее, чем внезапно заменить:
nameна:
projectNameи удалить старое.
Это делает backward compatibility особенно важной
Web можно обновить почти сразу.
Native users — нет.
Значит:
API contractстановится точкой совместимости нескольких поколений клиентов.
Иногда полезно передавать client version
Например:
X-App-Platform: android
X-App-Version: 4.7.2или эквивалент через API payload/telemetry.
Это помогает диагностировать:
Ошибка только у iOS 4.6?
или:
Проблема у всех клиентов?
Но сервер не должен превращаться в тысячу условий
Плохо:
if version < 3.2 ...
if version < 3.7 ...
if android 4.1 ...
if ios 4.2 ...Бесконечная поддержка старых контрактов тоже создаёт технический долг.
Нужна version support policy.
Второй release-вызов — кеш PWA
Web release может быть уже новым.
Но Service Worker пользователя ещё удерживает старые assets.
Теперь одновременно:
Web user A:
4.8
PWA user B:
4.7 cached
Android:
4.7
iOS:
4.6И все идут к одному API.
Вот почему управление версиями — не декоративная задача.
Намного безопаснее считать frontend клиентом API
Не:
Frontend и backend всегда обновляются одновременно.
А:
Backend должен пережить разумное окно существования старых клиентов.
Это особенно важно, если продукт одновременно PWA и native.
Следующий вопрос — deep links
На Web всё просто:
https://example.ru/day/2026-10-01Пользователь открывает URL.
В native app хочется получить:
ссылка
↓
открывается приложение
↓
тот же экран.Хорошо, когда route model общая
Например domain route:
/day/2026-10-01понимает и browser router, и native deep-link adapter.
Не нужно создавать:
web URLи совершенно отдельную систему native navigation IDs, если в этом нет необходимости.
Это одно из преимуществ web-first architecture
URL уже является хорошим идентификатором состояния интерфейса.
Его можно использовать как основу:
Browser routing
+
Deep link mappingНо native lifecycle отличается от браузерного
Например приложение:
было открыто;
ушло в background;
вернулось;или:
было полностью закрыто;
пользователь нажал deep link.Это разные сценарии.
Native adapter должен уметь передать событие основному router.
То же самое с кнопкой Back
Browser history:
Backи Android system back gesture/button — не всегда одно и то же.
Если просто оставить web navigation как есть, можно получить странный UX:
Android Back
→ закрывает приложениехотя пользователь ожидал:
вернуться на предыдущий экран.Так появляются небольшие platform adapters, которые нельзя увидеть при обычной desktop-разработке.
Safe Area — ещё один пример
Web на desktop:
top: 0означает верх экрана.
На современном телефоне есть:
status bar;
notch;
dynamic island;
system bars.UI должен учитывать safe areas.
В Capacitor 8 были обновлены подходы к edge-to-edge на Android, и документация рекомендует использовать соответствующие CSS env()/safe-area переменные для корректной компоновки интерфейса.
Это хороший пример того, почему:
Mobile responsive website
и:
Production native shell
всё-таки не полностью одинаковая задача.
Ещё один важный слой — клавиатура
На desktop форма выглядит идеально.
На телефоне открывается keyboard.
И внезапно:
нижняя кнопка исчезла;
modal перестала прокручиваться;
active input оказался под keyboard.Эти проблемы часто не проявляются в desktop responsive mode.
Поэтому native app нужно тестировать на реальных устройствах
Минимум:
Android
iOSИ желательно несколько классов экранов.
Мы не считаем тест Chrome DevTools полноценной проверкой Android
Responsive mode отлично помогает проверить layout.
Но он не моделирует полностью:
native WebView;
permissions;
system keyboard;
safe areas;
application lifecycle;
native plugins;
store build.Получается настоящая test matrix
Например:
| Сценарий | Browser | PWA | Android | iOS |
|---|---|---|---|---|
| Login | ✓ | ✓ | ✓ | ✓ |
| Offline shell | — | ✓ | ✓ | ✓ |
| Core data | ✓ | ✓ | ✓ | ✓ |
| File upload | ✓ | ✓ | ✓ | ✓ |
| Notifications | platform | platform | native | native |
| Deep link | URL | URL | ✓ | ✓ |
| Background/restore | browser | browser | ✓ | ✓ |
| Upgrade old version | web | SW | store | store |
Слово:
«кроссплатформенное»не означает:
«достаточно протестировать один Chrome».Но тестировать всё четыре раза тоже не обязательно
Хорошая архитектура позволяет разделить тесты.
Общие domain tests
Один раз:
задачи;
дневник;
валидация;
расчёты;
state machine.Web integration tests
Проверяют:
browser;
PWA;
Service Worker;
manifest;
cache update.Native smoke tests
Проверяют именно границу:
Capacitor starts;
native plugins;
permissions;
safe area;
deep links;
background/resume.Так мы не дублируем весь test suite без необходимости.
CI/CD тоже лучше разделить на ветви
Например:
Web source
│
▼
Build
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
Web Android iOS
│ │ │
deploy build build
│ │ │
PWA tests native QA native QAОдна ошибка web-core ломает все платформы.
Но platform-specific проблема остаётся локальной.
Это позволяет сохранить главное преимущество общего кода
Исправили:
расчёт;
форму;
историю задач;
API client.Изменение автоматически становится частью следующей версии:
Web
Android
iOS.Не нужно реализовывать одну business-функцию три раза.
При этом release timing может различаться
Web:
сегодня.Android:
после mobile build/release.iOS:
после своего release lifecycle.Поэтому:
один sourceне означает:
одновременный release.Это нужно учитывать архитектурно.
Что хранить локально
Ещё один важный вопрос гибридного приложения.
На Web можно использовать:
IndexedDB;
Cache Storage;
localStorageв зависимости от задачи.
В native приложении могут появиться дополнительные device storage возможности.
Не стоит позволять каждой платформе создать свою бизнес-модель
Плохо:
PWA data format A
Android data format B
iOS data format CЧерез год поддерживаются уже три системы синхронизации.
Лучше иметь одну domain serialization
Например:
{
"id": "...",
"updatedAt": "...",
"data": {}
}А platform storage adapter решает:
где физически сохранить.Особенно это важно для offline-first
Например пользователь изменил запись без сети.
Нам нужны:
local value;
server value;
base value;
revision;
conflict strategy.Эта логика должна быть общей.
А не:
Android sync algorithm
+
PWA sync algorithm.Иначе данные начнут вести себя по-разному в зависимости от способа установки приложения.
Capacitor — не замена backend
Иногда native shell создаёт ощущение:
Теперь приложение находится на телефоне, сервер нужен меньше.
Нет.
Если продукт использует:
аккаунты;
синхронизацию;
общие данные;
AI;
push;
cloud backup;backend по-прежнему остаётся центральной частью системы.
Capacitor меняет клиент.
Не всю архитектуру продукта.
И наоборот: PWA не делает backend необязательным
Service Worker способен помочь offline.
Но он не превращает сложную SaaS-систему в полностью автономную программу.
Следующая тема — безопасность
Web и native app всё равно считаются клиентами.
Нельзя рассуждать:
Запрос пришёл из нашего Android приложения, значит ему можно доверять.
Пользователь контролирует своё устройство.
API всё равно должен проверять:
authentication;
authorization;
ownership;
validation.Native shell не заменяет server-side permissions
Например UI скрывает:
[Удалить проект]на Android.
Злоумышленник всё равно может сформировать HTTP-запрос самостоятельно.
Поэтому все критичные проверки остаются на backend.
Secrets тоже нельзя просто положить в JavaScript bundle
Web assets native приложения можно анализировать.
Поэтому:
database password;
private backend key;
master encryption secretне должны становиться частью frontend только потому, что frontend упакован в APK/IPA.
Capacitor даёт дополнительные native-возможности, но не делает JavaScript сервером
Это важная граница.
А что с SEO?
Здесь двойная стратегия даёт интересное преимущество.
Native Android/iOS приложение само по себе не заменяет публичный Web.
Web/PWA остаётся доступным по обычным URL:
https://example.ruТо есть публичные страницы могут:
индексироваться;
получать organic traffic;
использоваться в рекламе;
получать внешние ссылки.Пользователь приходит из поиска
Например:
Google / Яндекс
↓
public web page
↓
web applicationПосле знакомства с продуктом он может:
продолжить в браузере;
установить PWA;
установить native app.Получается хорошая acquisition funnel.
Если сделать только native app
Органический search entry уже приходится решать отдельным маркетинговым сайтом.
Поэтому для web-first продукта сохранение Web часто очень полезно
Особенно если SEO — один из каналов привлечения.
Когда достаточно только PWA
Не каждому проекту нужен Capacitor.
Например приложение:
в основном формы;
таблицы;
контент;
личный кабинет;
задачи;и практически не требует native APIs.
Если пользователи спокойно устанавливают PWA и store presence не является бизнес-требованием, дополнительная Android/iOS инфраструктура может не окупиться.
PWA особенно сильна, если важны
моментальный вход по ссылке;
быстрые обновления;
SEO;
один deployment;
минимальная стоимость mobile support.Когда Capacitor становится интересным
Например нужны:
App Store / mobile distribution;
нативные уведомления;
device APIs;
filesystem;
camera;
platform integrations;
более привычный mobile installation flow.И при этом приложение уже имеет качественный web-interface.
Тогда Capacitor позволяет использовать накопленный web-код вместо полного переписывания продукта на две отдельные native codebase.
Когда поддержка обоих вариантов особенно оправдана
Есть несколько сильных условий.
1. У продукта уже есть web-аудитория
Нельзя сказать пользователям:
Теперь приложение существует только в store.
Web остаётся важным каналом.
2. SEO важно для привлечения
Публичный сайт и web pages должны продолжать жить.
3. Часть пользователей хочет обычное mobile app
Для них:
иконка;
store;
native notifications;
device integrationимеют ценность.
4. Большая часть UI и business logic универсальна
То есть Capacitor действительно позволяет переиспользовать значительную часть приложения.
5. Native features являются enhancement, а не полностью другим продуктом
Например:
основной дневникодинаков.
А native добавляет:
notification;
device integration.Это очень хороший сценарий.
Когда гибрид становится плохой идеей
Представим приложение почти полностью состоит из:
AR;
сложной 3D-графики;
Bluetooth;
background sensors;
heavy native media processing.А Web-часть — только небольшой экран.
В таком случае web-first architecture может уже не давать главного преимущества.
Нельзя выбирать Capacitor только чтобы поставить сайт в store
Если мобильный UX плохой:
мелкие кнопки;
desktop layout;
неудобные формы;
неработающий back;
сломанная keyboard;обёртка не исправит это автоматически.
Получится:
плохой мобильный сайт
в native package.Сначала приложение должно быть действительно mobile-friendly
И только потом native shell усиливает его.
Ещё один анти-паттерн — две независимые frontend codebase
Например:
web/
mobile/которые в первый день были одинаковыми.
Через год:
Web:
feature A/B/C/D
Mobile:
A/C/E
iOS:
ещё отдельные fixes.Главное преимущество Capacitor исчезло.
Поэтому мы стараемся держать различия на периферии
Common domain/UI
│
┌──────────┼──────────┐
│ │ │
Web adapter Android adapter iOS adapterНе:
Web app
Android app
iOS appкак три отдельных продукта.
Platform adapters нужно делать маленькими
Например:
notifications
files
share
device info
lifecycleНо:
task creation;
journal rules;
user profile;
analytics calculationsне должны становиться platform-specific.
Хороший тест архитектуры
Спросите:
Если завтра полностью убрать Capacitor, продолжит ли основная бизнес-логика работать в браузере?
Если:
да,web-core действительно независим.
Если половина domain импортирует:
@capacitor/...напрямую, граница уже начала разрушаться.
Импорты Capacitor полезно локализовать
Например:
platform/capacitor/а не разбросать:
@capacitor/coreпо сотне UI-компонентов.
Второй хороший тест
Если понадобится заменить Capacitor plugin одним собственным native plugin, сколько файлов изменится?
Если:
один adapter,архитектура хорошая.
Если:
сорок экранов,platform boundary слишком слабая.
Как обращаться с Service Worker
Мы бы рассматривали его как отдельный Web/PWA adapter.
Например:
platform/
├── native/
│ ├── notifications
│ └── lifecycle
│
└── web/
├── service-worker
├── pwa-install
└── web-pushService Worker не должен напрямую определять domain rules.
Например плохой вариант
если Service Worker cache имеет запись X,
значит задача выполнена.Cache — delivery/storage mechanism.
Статус задачи — business state.
Их нельзя смешивать.
Service Worker особенно важно тестировать при обновлениях
Например:
Install old PWA
↓
cache v12
↓
deploy v13
↓
reload/update
↓
старый cache удалён
↓
v13 работаетНе только:
новый browser profile
↓
v13 работает.Проблемы PWA чаще обнаруживаются именно у существующих пользователей.
Для native shell существует аналогичный upgrade test
Android v12 installed
↓
update to v13
↓
local data remains
↓
session works
↓
new schema compatibleТо же:
iOS.То есть нужно тестировать не только installation
Но и:
upgrade.В production это часто более важный сценарий.
Особенно если меняется local storage schema
Например старый клиент хранит:
{
"tasks": [...]
}Новый:
{
"tasksById": {}
}Просто установить новую версию на чистое устройство недостаточно.
Нужно проверить миграцию старых локальных данных.
Один web-core делает такие миграции проще
Если storage format общий, одна migration может работать:
PWA
Android
iOS.Platform adapter занимается только физическим persistence.
Не нужно забывать о store assets
Поддержка Capacitor означает, что теперь существуют:
app icon;
splash;
store screenshots;
privacy descriptions;
version numbers;
signing.Это отдельный release surface.
Web release может занимать минуты
Native release — уже самостоятельный operational процесс.
Поэтому перед решением:
Сделаем мобильную оболочку.
нужно считать не только стоимость первой реализации.
Но и стоимость каждого будущего релиза.
Это главный скрытый TCO
Теперь нужно поддерживать:
Android tooling;
iOS tooling;
Capacitor upgrades;
native plugins;
store requirements;
real devices;
signing.Например текущая ветка Capacitor 8 имеет свои требования к Xcode, Android Studio, SDK и platform target, то есть native toolchain тоже движется независимо от вашего web-продукта.
Поэтому Capacitor — не «бесплатный APK из сайта»
Он экономит огромное количество дублирования web-логики.
Но native application lifecycle всё равно существует.
Это нормальный trade-off
Мы получаем:
один web-core
+
Android
+
iOSвместо:
Web team
+
Kotlin application
+
Swift applicationНо цена не становится нулевой.
Практичная структура проекта
Например:
project/
│
├── src/
│ ├── domain/
│ ├── api/
│ ├── ui/
│ ├── offline/
│ └── platform/
│ ├── web/
│ └── native/
│
├── public/
│ ├── manifest.webmanifest
│ └── icons/
│
├── service-worker/
│
├── android/
├── ios/
│
└── capacitor.config.*Здесь видно:
domainне принадлежит ни одной платформе.
А:
android/
ios/остаются настоящими native projects.
Как выглядит build
Концептуально:
Shared source
↓
Web build
│
├────→ Deploy Web/PWA
│
├────→ Capacitor Android
│
└────→ Capacitor iOSCapacitor использует директорию собранных web-assets как источник для native приложения.
Это и позволяет реально переиспользовать один frontend build.
Но конфигурации сред могут отличаться
Например:
Web production API:
https://api.example.ruNative тоже должен обращаться туда же.
Нельзя случайно собрать Android:
API_BASE_URL=http://localhost:4173и обнаружить это после публикации.
Поэтому native build нужен собственный production preflight
Проверяем:
API URL;
build mode;
app ID;
version;
plugins;
permissions;
icons;
signing configuration.Так же как web deployment имеет собственный preflight.
Какой вариант дешевле поддерживать
Если нужна только таблица, форма и профиль:
PWAпочти наверняка дешевле.
Если уже нужны:
store distribution;
native notifications;
device APIs;
сильная mobile integration,Capacitor начинает оправдывать дополнительную стоимость.
А поддержка обоих вариантов оправдана, когда у них разные задачи
Это ключевой вывод.
PWA отвечает:
Как максимально быстро доставить продукт пользователю через Web?
Capacitor:
Как этот же продукт глубже встроить в Android/iOS?
Это не один и тот же вопрос.
Сравним
| Возможность | Web/PWA | Capacitor Android/iOS |
|---|---|---|
| Открытие по ссылке | Отлично | Нужна установка |
| SEO/public Web | Да | Само приложение — нет |
| Быстрый web-релиз | Да | Native package живёт отдельным циклом |
| Установка без store | Да | Обычно native distribution |
| Offline shell | Service Worker/cache | Packaged web assets |
| Device APIs | Через доступные Web API | Через native plugins |
| Native permissions | Ограничено web-моделью | Да |
| Store presence | Не основной путь | Да |
| Единый web-core | Да | Да |
| Native maintenance | Нет | Да |
| Deep OS integration | Ограничена платформой/browser | Значительно шире |
Когда мы бы выбрали только PWA
Если:
основной канал — Web;
SEO важно;
native API почти не нужны;
команда небольшая;
минимальный TCO важен.Когда выбрали бы PWA + Capacitor
Если:
Web остаётся важным;
нужны Android/iOS;
большая часть UI общая;
нужны native APIs;
store presence полезен;
продукт должен работать и по ссылке, и как mobile app.Когда web-first подход уже может не подойти
Если мобильное приложение по своей природе в основном:
native sensors;
Bluetooth;
AR;
сложная background activity;
high-performance graphics;
глубокая platform integration.Тогда доля общего Web-кода может стать слишком маленькой.
Практический checklist PWA + Capacitor
Перед тем как поддерживать оба канала, мы бы проверили:
- основная business logic не зависит от Capacitor;
- Web остаётся полноценным клиентом продукта;
- manifest и PWA installation проверяются отдельно;
- Service Worker имеет версионирование cache и upgrade-test;
- старые PWA-клиенты корректно переходят на новый release;
- Capacitor imports локализованы в platform layer;
- native plugin имеет web fallback, если функция должна существовать в браузере;
- native permissions запрашиваются осознанно;
- Android back/navigation протестированы;
- iOS/Android safe areas проверены;
- keyboard не ломает формы и modal;
- deep links ведут в ту же domain navigation model;
- backend API совместим с несколькими версиями mobile clients;
- application version можно диагностировать на сервере;
- upgrade Android/iOS проверяется поверх предыдущей версии;
- local user data не теряется после native update;
- offline shell не путается с offline business data;
- sync logic является общей, а не отдельной для каждой платформы;
- критичные права доступа всегда проверяются backend;
- secrets не зашиваются во frontend bundle;
- web/PWA и native имеют отдельные smoke-tests;
- Android/iOS тестируются хотя бы на реальных устройствах;
- native toolchain обновляется контролируемо;
- store release рассматривается как отдельный production-процесс.
Самый важный архитектурный тест
Удаляем мысленно:
android/
ios/Работает ли Web?
Если нет, мы создали слишком сильную зависимость от оболочки.
Второй тест
Удаляем PWA features:
Service Worker;
install prompt.Открывается ли сайт как обычное web-приложение?
Если нет, progressive enhancement тоже нарушен.
Третий тест
Backend видит клиента:
Web 5.2
Android 5.0
iOS 4.9Продолжают ли работать критичные сценарии?
Если нет — release model слишком жёстко связывает server и mobile clients.
Четвёртый тест
Пользователь потерял интернет.
Что именно продолжает работать?
Нужно уметь перечислить это явно:
Application shell ✓
Existing local data ✓
Create local record ✓
Server synchronization ✗или:
Application shell ✓
Business data ✗Оба варианта допустимы.
Опасно только не знать, какой из них реализован.
Что мы получили в итоге
Не:
сайт
+
ещё одно Android приложение
+
ещё одно iOS приложение.А:
Shared web product
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Browser PWA Native shell
│
┌─────┴─────┐
▼ ▼
Android iOSУ продукта остаётся одна domain model.
Один API.
Основной UI.
Общая offline/sync logic.
Но capabilities расширяются в зависимости от платформы.
Вместо вывода
Выбор между PWA и нативным приложением часто формулируют слишком жёстко:
Что выбрать?
Для web-first продукта иногда правильнее задать другой вопрос:
«Нужно ли нам вообще выбирать только одно?»
PWA даёт очень сильные свойства Web:
открытие по ссылке;
SEO;
моментальную доступность;
быстрый deployment;
установку без отдельного native codebase.Capacitor добавляет:
Android;
iOS;
native plugins;
device APIs;
store distribution;
platform integration.И при правильной архитектуре один вариант не обязан уничтожать другой.
Ключевой принцип — сохранять общими:
domain;
UI;
API client;
business rules;
sync logic.А различия концентрировать на границе:
notifications;
permissions;
files;
deep links;
lifecycle;
device APIs.Тогда Capacitor действительно работает как нативная оболочка вокруг web-first продукта, а не как начало второго независимого приложения.
Service Worker при этом продолжает решать web/PWA-задачи.
Native package имеет собственный lifecycle.
Backend учитывает несколько поколений клиентов.
А пользователь может выбрать наиболее удобный путь:
открыть сайтили:
установить PWAили:
использовать Android/iOS приложение.Это немного дороже в поддержке, чем один только Web.
Но для продукта, который одновременно хочет сохранять открытость Web и получить полноценное присутствие на мобильных платформах, такой подход даёт очень хороший компромисс:
одна основа продукта — несколько способов доставки пользователю.