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-тест от преждевременного: решение по цене и пользе, а не по привычке.