Web-first подход — фронтенд на React + TypeScript, упакованный в Capacitor или PWA — закрывает большинство продуктовых задач одним стеком. Но у него есть честная граница, за которой WebView начинает мешать, а не помогать. Продукт-инженеру важно знать, где эта граница проходит, и какие есть варианты, когда в неё упёрся, — чтобы переход на натив был осознанным решением, а не паникой посреди релиза.
Где web-first упирается
WebView — это браузер внутри приложения, и он наследует ограничения браузера. Есть классы задач, где этого не хватает:
- Тяжёлая графика и анимация. 3D-сцены, игровые движки, интерфейс с десятками одновременных переходов на 120 Гц — WebView упирается в потолок производительности рендеринга раньше натива.
- Сложные жесты. Многопальцевые манипуляции, инерционные взаимодействия, тонкая обработка касаний — браузерная модель событий даёт ощутимо менее точный отклик.
- Глубокая нативная интеграция. Виджеты на домашнем экране, Apple Watch, CarPlay, системные расширения — это вне досягаемости web-слоя в принципе.
- Пиковая производительность. Обработка изображений и видео в реальном времени, сложные вычисления на устройстве — там, где важна каждая миллисекунда, JS-runtime проигрывает машинному коду.
- AR и тяжёлая работа с камерой. Дополненная реальность, ручное управление параметрами камеры, покадровая обработка потока — браузерные API здесь либо неполны, либо недоступны.
- Фоновые задачи. Надёжная работа после закрытия приложения — синхронизация, геотрекинг, длинные выгрузки. Браузер останавливает фон, когда вкладка уходит; натив — нет.
Через нативные плагины Capacitor часть этого добирается, но если ядро продукта — именно такая задача, мост через WebView становится узким горлом, а не мостом.
React Native
Если упёрся, но команда — это фронтенд, ближайшая ступень — React Native. Логика и интерфейс пишутся на JavaScript/TypeScript, но React Native не рисует HTML в WebView, а рендерит настоящие нативные виджеты платформы (UIView на iOS, android.view.View на Android). Отсюда нативный отклик и нативный внешний вид при сохранении React-модели.
import { View, Text, Pressable } from "react-native";
export function Counter({ value, onInc }: { value: number; onInc: () => void }) {
return (
<View>
<Text>{value}</Text>
<Pressable onPress={onInc}>
<Text>Increment</Text>
</Pressable>
</View>
);
}
Главное преимущество — переиспользование навыков: те же компоненты, хуки, управление состоянием, та же загрузка данных. Меняется слой представления (View/Text вместо div/span) и доступ к платформе. Это не бесплатно — появляется отдельная сборка, нативные зависимости, свои подводные камни, — но порог входа для React-команды ниже, чем у любой другой нативной альтернативы.
Flutter
Flutter решает ту же задачу иначе. Язык — Dart (не JS/TS), а UI рисует собственный движок рендеринга через GPU (Metal на iOS, Vulkan/OpenGL на Android), не делегируя отрисовку платформенным виджетам. Отсюда полная визуальная идентичность между iOS и Android и стабильно высокая производительность анимации — но и отрыв от платформенного look-and-feel, который иногда нужен наоборот.
ElevatedButton(
onPressed: onInc,
child: Text('$value'),
)
Для фронтенд-команды Flutter — это смена языка и экосистемы целиком: Dart, свои инструменты, свой пакетный менеджер. Технически зрелый выбор, особенно когда нужен единый пиксель-в-пиксель интерфейс и тяжёлая графика, но переиспользования React-навыков здесь практически нет — это инвестиция в новый стек.
Полный натив: Swift и Kotlin
Крайняя ступень — отдельные приложения на родных языках: Swift для iOS, Kotlin для Android. Это максимум возможностей и контроля: любой системный API в день выхода, пиковая производительность, идеальная интеграция с платформой. Цена — две раздельные кодовые базы, две команды компетенций, самая высокая стоимость разработки и поддержки.
struct CounterView: View {
@State private var value = 0
var body: some View {
Button("\(value)") { value += 1 }
}
}
Полный натив оправдан, когда нативность — не деталь, а суть продукта: профессиональная камера, AR-платформа, требовательная к железу обработка, глубокие системные расширения. Для большинства продуктов это перебор, но там, где он нужен, ничто другое не закрывает задачу.
Как решать
Выбор делается не по моде, а по трём вопросам:
- Насколько критичен нативный UX. Если расхождение в пару кадров и не-родной отклик убивают продукт — web-first и Capacitor не подойдут. Если пользователь не заметит — подойдут.
- Есть ли уникальные нативные требования. AR, фоновый геотрекинг, виджеты, профессиональная камера — конкретная возможность, которой web-слой не даёт, перевешивает удобство единого стека.
- Бюджет и команда. Фронтенд-команда и сжатый бюджет тянут к web-first или React Native; две раздельные команды и высокие ставки делают полный натив реалистичным.
Здравая последовательность по росту стоимости и контроля: PWA → Capacitor (с упаковкой через WKWebView на iOS и TWA на Android) → React Native → Flutter → полный натив. Двигаться вправо стоит только тогда, когда упёрся в конкретный потолок, а не превентивно.
Где это в UCP
Выбор технологии мобильного клиента — это то же инженерное решение по границе, что и выбор слоя в backend: берём самый дешёвый инструмент, который закрывает задачу, и усложняем только по доказанной необходимости. Продукт-инженер начинает с web-first, потому что он переиспользует frontend-стек и даёт скорость, и переходит на натив осознанно — под конкретное требование, а не из страха «вдруг не хватит». Эта дисциплина «не усложнять раньше времени» — та же, что отделяет оправданный сквозной e2e-тест от преждевременного: решение по цене и пользе, а не по привычке.