Каждый технический директор, выбирающий стек для мобильного продукта, слышал готовый ответ: Flutter рисует быстрее, React Native удобнее командам. Готовые ответы опасны тем, что верны не везде. Один движок контролирует каждый кадр сам и ускоряет анимации, второй превращает знакомый React в нативные виджеты и позволяет переиспользовать людей с веба. Ниже разбор механики обеих платформ, реальные различия на сложных экранах и честный взгляд на то, где заканчиваются графики из презентаций и начинается практика конкретной команды.

Два разных принципа отрисовки интерфейса на экране

Flutter собирает приложение из собственных виджетов и рисует их сам. Код на Dart компилируется в машинный код под ARM, сцена строится на потоке Dart и передаётся растровому потоку движка отрисовки. Исторически движком служил Skia, с 2023 года на iOS основным стал Impeller, в 2024-м он закрепился и на Android. Практическая разница для пользователя одна: Impeller заранее компилирует шейдеры, и первая прокрутка сложного экрана не спотыкается о фриз, знакомый каждому по старым сборкам.

React Native работает иначе: JavaScript описывает дерево компонентов, рендерер Fabric отражает его в нативные виджеты платформы, компоновкой занимается движок Yoga, его версия 3.0 приехала с релизом RN 0.74 в апреле 2024 года. Связь между скриптом и нативным слоем держит интерфейс JSI: с релиза 0.76 в октябре 2024 года новая архитектура включена по умолчанию, старый мост с сериализацией в JSON убран, вызовы ходят напрямую через C++. Движок Hermes, ставший умолчанием с версии 0.70 в сентябре 2022 года, компилирует скрипт в байткод заранее и экономит память на старте.

Итог противостояния принципов: Flutter предсказуемо быстр, потому что не зависит от платформенных виджетов, React Native нативен, потому что его компоненты и есть платформа.

Производительность сложных списков и анимаций в живых проектах

Бюджет кадра прост: 16.6 мс при 60 кадрах в секунду, 8.3 мс при 120. Любая просадка означает дёрганье, и на сложном экране оба стека тратят бюджет по-разному. Flutter делит кадр между двумя потоками: код на Dart строит сцену, растровый поток отдаёт её видеоядру. Сложный список собирается через фабричный конструктор ListView.builder, и внеэкранные элементы не создаются вовсе:

ListView.builder(
  itemCount: catalog.length,
  itemBuilder: (context, i) => ProductTile(
    key: Key(catalog[i].id),
    product: catalog[i],
  ),
);

Помеченные словом const конструкторы позволяют Dart кэшировать неизменяемые виджеты, и перестройка кадра трогает только изменённые узлы дерева. Профилировщик DevTools размечает каждый кадр по потокам и подсвечивает просадки красным, искать источник фриза удобно прямо на устройстве.

React Native проходит более длинный путь: событие прокрутки рождается в нативном слое, обработчик в JavaScript возвращает решение об отрисовке, рендерер Fabric применяет разметку. Долгая задача в скрипте, например разбор ответа на 5 МБ в формате JSON, замораживает и список, и анимацию. Новая архитектура убрала сериализацию и сделала компоновку синхронной, но практика осталась прежней: тяжёлое держат в нативном слое, а список настраивают под ограничения:

<FlatList
  data={catalog}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <ProductTile product={item} />}
  windowSize={5}
  initialNumToRender={12}
  getItemLayout={getRowHeight}
/>

Анимации выносят с потока скрипта: библиотека Reanimated исполняет декларативные анимации прямо на потоке интерфейса, жесты забирает Gesture Handler, готовый список с переработкой ячеек даёт FlashList. Команда, которая пользуется этими инструментами, не видит разницы между стеками на обычных экранах. Команда, которая не пользуется, спотыкается на каждом сложном списке одинаково.

Замеры на реальных проектах сходятся в одном: на устройствах среднего класса четырёх-пятилетней давности тяжёлые экраны с плотной анимацией чаще ведут себя ровнее у Flutter, простые экраны неотличимы. Приговор React Native из времён старого моста сегодня смягчён, и решение принимают не по слухам, а по протоколу замеров на целевых устройствах.

Скорость итераций и горячая перезагрузка на обеих платформах

Горячая перезагрузка Flutter пересобирает дерево виджетов за доли секунды и сохраняет состояние экрана. Быстрая перезагрузка React Native обновляет изменённый модуль за секунду-две и старается удержать состояние хуков. По чистой скорости отклика на правку оба инструмента закрывают потребность: разработчик не уходит пить кофе после каждой смены отступа.

Различия вылезают уровнем выше. Сборка React Native идёт через Gradle и Xcode, холодная пересборка на сервере сборки занимает минуты, инкрементальные прогоны спасают только локально. Flutter собирается проще, но любое изменение платформенного слоя опять запускает Gradle. Скорость итераций команды определяет не одна перезагрузка, а стоимость полного цикла: сборка, проверка на реальных устройствах, раздача тестовой сборки.

Тесты добавляют к картине отдельную строку расходов. Команда flutter test гоняет тысячи виджет-тестов без эмулятора за десятки секунд: каждый тест собирает дерево виджетов, прогоняет событие и проверяет состояние. У React Native тесты компонентов крутятся в Jest с подменёнными нативными модулями, тоже без устройства, но проверка реального нативного слоя требует телефона. Баланс похожий: быстрый юнит-слой есть у обоих стеков, интеграционные расходы тоже лежат на обоих.

Тут у React Native есть козырь, о котором часто забывают в технических спорах: обновления скрипта поверх магазинов. Механизм EAS Update из набора Expo доставляет свежий бандл пользователям, минуя проверку магазина, за минуты: ошибка в тексте или раскладке каталога лечится патчем до обеда. У Flutter официального механизма таких обновлений нет, собранный машинный код меняется только новым пакетом в магазине, сторонние сервисы патчей существуют, но ответственность за них лежит вне фреймворка. Для продуктов с частыми экспериментами над контентом это весомый аргумент.

Пул разработчиков и переиспользование кода между платформами

React Native говорит на языке, который знает каждый второй фронтенд-разработчик. Каталог npm давно перевалил за два миллиона пакетов, TypeScript разделяет типы между сайтом и приложением, команда из четырёх человек покрывает и веб, и мобильный экран, обмениваясь людьми. Dart за пределами Flutter встречается редко: найм идёт медленнее, новый человек входит в проект неделями, а не днями.

У Flutter есть ответ: единый код на iOS, Android, Windows, macOS, Linux и в браузере, один слой бизнес-логики, общие тесты, один язык на всю компанию. На практике веб-цель Flutter ограничена: отрисовка в графический слой браузера тяжела и не заменяет обычную страницу для поисковой индексации. React Native страдает зеркальной болезнью: вне мобильных платформ он используется редко, зато связка с react-native-web переиспользует компоненты в браузере почти без переделок.

Итог по людям выглядит честно так: Flutter покупает единообразие, React Native покупает гибкость найма. Для аутсорсной команды с ротацией кадров второе часто перевешивает красоту анимаций. Для продукта с долгой жизнью и шестью целевыми платформами первое экономит годы.

Долгосрочная стоимость сопровождения похожа у обеих платформ: по три-четыре стабильные версии в год, обновление крупного проекта занимает от пары дней до пары недель, нативные модули и плагины мигрируют отдельно от кода приложения. Эту статью расходов закладывают в план до старта, а не после первой ошибки обновления.

Включение и настройка New Architecture React Native в существующем проекте

С релиза 0.76 новая архитектура включена по умолчанию, свежий шаблон работает на Fabric и TurboModules сразу. В старых проектах флаги живут в файлах сборки: для Android это gradle.properties, для iOS переменная окружения в Podfile:

# android/gradle.properties
newArchEnabled=true
hermesEnabled=true

# ios/Podfile
ENV["RCT_NEW_ARCH_ENABLED"] = "1"

Что меняет включение: TurboModules инициализируются лениво, старт приложения ускоряется; Fabric выполняет компоновку синхронно, приоритет жестов и прокрутки обрабатывается на стороне платформы; сериализация JSON между потоками исчезает. В коде приложения изменения почти не видны, но протоколы замеров старта показывают сокращение времени до первого экрана.

Развитие ветки продолжает ту же линию: релиз 0.77 в 2025 году добавил сборку под 16-килобайтные страницы памяти, требование Google для новых Android-устройств, релиз 0.78 привёз React 19. Проект живёт в бодром темпе: мажорные версии выходят несколько раз в год, и команда, сидящая на ветке годичной давности, платит временем миграции. Flutter в этом смысле спокойнее: стабильные версии 3.24 из августа 2024 года и 3.27 из декабря 2024-го обновляются без ломающих изменений внутри ветки.

Инструменты профилирования и поиск просадок кадров

Flutter DevTools показывает таймлайн кадра с двумя потоками, размечает потери бюджета красным и сохраняет трейсы для сравнения между релизами. Запуск в режиме профиля обязателен: отладочный режим с отключёнными оптимизациями искажает картину. Команды для замеров выглядят так:

flutter run --profile
flutter build apk --analyze-size

React Native DevTools с релиза 0.76 включает профилировщик с деревом React-компонентов и интеграцией с Hermes: видно, какой компонент выходит за бюджет кадра. Нативные трейсы снимает Perfetto, а статистику кадров на Android даёт системная утилита:

adb shell dumpsys gfxinfo com.example.shop

Старт приложения замеряется системной командой, одинаковой для обоих стеков, она печатает время до первого экрана:

adb shell am start -W com.example.shop/.MainActivity

Размер сборки смотрят штатными средствами. У Flutter флаг сборки раскладывает приложение по архитектурам процессора, у React Native релизный выпуск идёт через Gradle, а состав пакета показывает Android Studio:

flutter build apk --split-per-abi
cd android && ./gradlew assembleRelease

Правило для обоих стеков одно: мерить на устройствах нижнего ценового сегмента, которые реально купили пользователи. Флагман с 12 ГБ памяти скрывает и лишние аллокации, и кривой список, и просадки на старте, и решение, принятое по замерам на флагмане, всегда оказывается решением о чужом приложении.

Реальный выбор стека под тип приложения и состав команды

Flutter подходит продуктам с тяжёлой графикой: интерфейсы с плотной кастомной анимацией, нестандартные переходы, интерактивные каталоги, редакторы и визуализация данных. Плюс один код на шесть платформ и предсказуемая отрисовка на слабых устройствах. React Native подходит там, где важнее скорость продукта: частые релизы, эксперименты над контентом, обновления поверх магазинов, команда уже пишет на React для веба, а интерфейс следует гайдлайнам платформ и не гонится за нестандартной графикой.

Решение обычно сходится к трём пунктам:

  1. если интерфейс насыщен кастомной анимацией и должен быть одинаковым до пикселя на любом устройстве, Flutter снимает целый класс проблем;
  2. если команда живёт в React и выпускает правки несколько раз в неделю, React Native экономит недели найма и дни на каждом релизе;
  3. если приложение несложное, у обеих платформ не останется решающих преимуществ, и выбирать нужно по людям под рукой, а не по чужим бенчмаркам.

Универсального победителя в паре нет: один стек покупает контроль над каждым кадром ценой отдельного языка и медленного найма, второй покупает скорость команды ценой аккуратной работы с тяжёлыми экранами. Смотреть нужно на конкретный продукт: количество анимаций, частоту релизов, целевые устройства, доступных людей. Бенчмарки из презентаций дают отправную точку, но решение принимает протокол замеров на своих экранах и своих устройствах. Такой подход занимает пару спринтов и экономит годы переделок.