Когда мы только начинали разработку мобильного приложения, выбор пал на React Native — казалось, это идеальный компромисс между скоростью разработки и кроссплатформенностью. Однако, со временем мы столкнулись с рядом проблем: низкая производительность на слабых Android-устройствах, сложность поддержки MapKit SDK, нестабильная работа некоторых библиотек и отсутствие нормальных dev-tools.

Основной фишкой приложения была интерактивная карта: отображался маршрут движения для водителя и более 10 000 объектов на экране одновременно. Для этого использовалась виртуализация, а в некоторых сценариях - еще и сортировка обьектов по маршруту движения. С каждой версией функциональность карты становилась все сложнее и тяжелее - что быстро начало сказываться на производительности.

Более того, мы довольно быстро пришли к выводу, что адекватной обертки для MapKit SDK под React Native попросту нет. Единственный существующий пакет https://github.com/volga-volga/react-native-yamap не реализовывал большую часть необходимого функционала: отсутствовала кастомизация кластеров, нельзя было анимировать обьекты, не поддерживался оффлайн-режим.

Самой критичной проблемой стал баг, из-за которого при загрузке нового js-бандла через CodePush(на данный момент уже не поддерживается) полностью пропадали все локальные иконки на карте. В результате мы приняли решение форкнуть существующую библиотеку и дописать весь необходимый функционал самостоятельно - от кластеризация до кастомных иконок и анимации.

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

Почему выбрали Flutter?

В мире мобильной разработки на текущий момент видна существенная тенденция на переход в кроссплатформу. Да, хочется и рыбку съесть и ... сразу на две платформы приложение написать. А конкурентов здесь не так и много: Flutter, Kotlin Multiplatform, React Native.

Kotlin Multiplatform показался слишком сырым для полноценной разработки в небольшой команде. Да, бизнес-логику можно было бы переиспользовать, но UI по-прежнему пришлось бы писать отдельно для iOS и Android - а это сильно снижает выйгрышь по времени и усилиям. Кроме того, до недавнего времени существовала проблема с фризами, связанные с работой сборщика мусора. А из-за молодого комьюнити выбор готовых библиотек оказался бы ограниченным, и многое приходилось бы реализовывать вручную.

На мой взгляд, KMP отлично подходит для готовых продуктов, которые уже написаны на Kotlin и хотят добавить поддержку iOS с минимальными затратами.

Процесс миграции

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

Уже на первых этапах я ощутил всю мощь этого инструмента - здесь все сделано с прицелом на разработчика. Удобный CLI, развитый пакетный менеджер pub.dev, встроенный DevTools с профилировщиком, инспектором и визуализатором слоев - все это реально ускоряет разработку и помогает быстро отлавливать узкие места в производительности.

Если в команде есть хороший дизайнер с проработанной дизайн системой - скорость разработки возрастает еще больше - в проекте можно заранее определить темы, цветовые схемы, стили текста и отдельные виды компонентов (например, BottomSheet, AppBar, Switcher) - то, чего не хватало в React Native "из коробки". После этой проделанной работы компоненты будут выглядеть аккуратно, без лишней верстки и стилей.

Изначально, не очень хотелось садиться за Flutter из-за непонятного языка Dart, который больше нигде не используется. Однако уже в первый день бОльшую часть синтаксиса я все же усвоил: Dart оказался удивительно похож на JavaScript. К тому же мне сильно помог предыдущий опыт с ООП-языками - С++, C#, Kotlin.

Наложив свой прошлый опыт написания навигатора на Jetpack Compose, где архитектурные концепции и подходы во многом схожи, освоение нового фреймворка показалось довольно быстрым.

В конечном итоге команда из 3 разработчиков переписала существующее приложение на Flutter за полгода. При этом проект был далеко не маленький и точно не из простых: более 60 экранов, настроенные push-уведомления, deeplinks, реализован оффлайн-режим, активная работа с камерой, а также использование нативных модулей.

Каков результат? Что мы получили?

  1. Стабильность

    У собственного движка отрисовки есть свои плюсы - это будет выглядеть и работать одинаково на всех платформах, с багами или без.

    Ранее, при использовании React Native, мы сталкивались с ситуациями, когда на определенных устройствах(Samsung, Oppo, Honor) происходили визуальные баги - часть контента не отображалась, но обработчики продолжали отрабатывать.

    После закрытия камеры контент предыдущий страницы исчез
    После закрытия камеры контент предыдущий страницы исчез
    Часть BottomSheet не видно
    Часть BottomSheet не видно
  2. Производительность

    Средний FPS вырос на глазах - на тестовом устройстве за 10 000 рублей - Huawei nova Y9, купленном 2.5 года назад - среднее значение при использовании приложения 85 кадров в секунду!

    Но еще больше меня удивило другое: пользователи iOS начали сами писать в чаты, что интерфейс стал заметно плавнее и отзывчивее.

    Добиться такого уровня производительности удалось в том числе благодаря выносу тяжелой логики в отдельный изолят (если упрощать - в отдельный поток).

    Одна из главных проблем React Native - эта работа приложения в одном основном потоке. В результате любые ресурсоемкий задачи - например, обработка большого количество объектов для виртуализации и кластеризация или построение маршрута - могли легко заблокировать основной UI thread, приходилось использовать более оптимизированные алгоритмы и дробить операции на Promise'ы.

    Начиная с Flutter 3.7, появилась возможность выносить в отдельный изолят не только dart'овые вычисления , но и вызовы нативных модулей. Теперь мы обрабатываем данные из базы, парсим большие ответы с сервера и выполняем тяжелые вычисления вне основного потока. Результат был виден сразу - интерфейс оставался плавным и отзывчивым.

    Для удобства мы написали свой менеджер взаимодействия с изолятами, чтобы централизовать и упростить передачу данных между потоками. Ниже представлена основная часть кода.

    class WorkerManager {
      static final WorkerManager _instance = WorkerManager._internal();
    
      factory WorkerManager() => _instance;
    
      WorkerManager._internal();
    
      List<Worker> _workers = [];
      Queue<Task> _taskQueue = Queue();
    
      Map<Capability, Completer> _activeTaskCompleters = {};
    
      Future<void> turnOn({int workersCount = 2}) async {
        _workers = [];
        _taskQueue = Queue();
        _activeTaskCompleters = {};
    
        for (int i = 0; i < workersCount; i++) {
          Worker worker = Worker();
          await worker.init(onResult: _onTaskFinished);
          _workers.add(worker);
        }
      }
    
      Future<T> compute<T, Y>(T Function(Y) fn, {Y? param}) async {
        final taskCapability = Capability();
        final taskCompleter = Completer();
    
        final Task task = Task(param: param, task: fn, capability: taskCapability);
    
        _activeTaskCompleters[taskCapability] = taskCompleter;
    
        Worker? freeWorker;
        for (final worker in _workers) {
          if (worker.status == WorkerStatus.idle) {
            freeWorker = worker;
            break;
          }
        }
        if (freeWorker == null) {
          _taskQueue.add(task);
        } else {
          freeWorker.execute(task);
        }
        T result = await taskCompleter.future;
        return result;
      }
    
      Future<void> turnOff() async {
        for (Worker worker in _workers) {
          await worker.dispose();
        }
      }
    
      void _onTaskFinished(TaskResult result, Worker worker) {
        Completer? taskCompleter = _activeTaskCompleters.remove(result.capability);
        taskCompleter?.complete(result.result);
    
        if (_taskQueue.isNotEmpty) {
          final task = _taskQueue.removeFirst();
          worker.execute(task);
        }
      }
    }

    Прогнав приложение через профайлер получили следующее:

    Потребление оперативной памяти на двух технологиях практически одинаковое

    Flutter - потребление в пике 578мб
    Flutter - потребление в пике 578мб
    React Native - потребление в пике 545мб
    React Native - потребление в пике 545мб
  3. Нюансы

    Безусловно, Flutter мощный инструмент, но и у него есть свои проблемы, с которыми мы продолжаем жить.

    1. Отрисовка изображений

      Эта проблема существует с самого появления фреймворка и до сих пор вызывает дискомфорт. Из-за особенностей отрисовки ресурсов изображение не появляется сразу:

      • На карте, например, иконки через MapKit SDK могут не успеть отрисоваться вовремя — вместо маркеров пользователь видит “пустой” круг

      • На первом экране онбординга в течение первых 100-200 мс вместо фоновой картинки пользователь видит белый экран, что особенно раздражает.

        Частично это фиксится с помощью функции precacheImage, подгружая изображения заранее. Но согласитесь, если вы впервые запускаете приложение и видите "белое полотно" вместо фона - впечатление будет испорчено.

    2. Фризы при первых анимациях

      Первое с чем мы столкнулись при работе с Flutter - мелкий подтормаживания при первой отрисовке анимированных элементов.

      Почему это происходит?

      На текущий момент практически все приложения, которые написаны на Flutter, используют движок Skia, у которого есть важная особенность: шейдеры компилируются "на лету".

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

      При этом есть возможность провести "фейковую" отрисовку, чтобы скомпилировать заранее соответствующий шейдер до запуска основного UI:

      void main() {
        runApp(const MyApp());
      
        // Кастомный прогрев шейдеров
        final warmUp = MyShaderWarmUp();
        warmUp.execute();
      }
      
      class MyShaderWarmUp extends DefaultShaderWarmUp {
        @override
        void warmUpOnCanvas(Canvas canvas, Size size) {
          final paint = Paint()..shader = const LinearGradient(
            colors: [Color(0xFFE91E63), Color(0xFF2196F3)],
          ).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
      
          // Принудительная отрисовка сложных элементов
          final rect = Rect.fromLTWH(0, 0, size.width, size.height);
          canvas.drawRRect(
            RRect.fromRectAndRadius(rect, const Radius.circular(20)),
            paint,
          );
      
          // Можно добавить другие сложные элементы (тени, клиппинг и т.д.)
        }
      }

      Flutter активно развивает новый движок отрисовки - Impeller, призванный решить эту проблем путем предсобранных шейдеров. Однако он остается нестабилен для многих устройств Android. Мы столкнули с этим на собственном опыте во время наших тестов - дешевое устройства работало идеально, выдавая максимальную производительность без намека на подтормаживания, но запустив тот же APK файл на Samsung Galaxy A55 - тормоза были видны при открытии простого BottomSheet.

    3. Обработка Deeplink

      Мы используем разделение навигации на авторизованную и неавторизоованную части. И здесь есть подводный камень: если приложение было запущенно по deeplink'у, а потом пользователь разлогинился и роутер пересоздал, примениться старый deeplink к уже неактуальному состоянию. Чтобы исправить эту ошибку, придется писать дополнительную логику и вводить проверки.

Итоги

Перевод приложения с React Native на Flutter оказался большим, но оправданным шагом. Мы не просто переписали приложение - мы улучшили архитектуру, улучшили UX, избавились от накопившихся технических ограничений и получили стабильный и производительный продукт.

Такой переход ускорил Time To Market - теперь команда сфокусирована на разработке новых фичей, а не на исправление багов.

Отдельно стоит сказать про выбор технологии с учетом реальной картины на рынке.

Flutter на сегодня самый актуальный и популярный инструмент для разработки кроссплатформенных мобильных приложений в СНГ. Почему это важно?

  • Больше разработчиков на рынке - в отличии от React Native, где часто встречаются frontend-разработчики, переехавшие из web, без понимания мобильной специфики

  • Больше библиотек и SDK с официальной поддержкой - многие SDK(например, Yandex, 2GIS, VK SDK, платежный шлюзы) поддерживают только Flutter-обертки

  • Стабильность. С каждым разом Meta выпиливает из ядра RN ключевые части, из-за чего на каждый чих приходится залазить на NPM и искать библиотеку, за поддержку которой никто не отвечает. Например, для нормальной анимации нужно подтягивать react-native-reanimated, а для корректной работы с safeArea - react-native-safe-area. Во Flutter же дела обстоят иначе - необходимые инструменты уже встроены в SDK, крупные библиотеки поддерживаются самим Google, а для публикации пакета на pub.dev автор обязан пройти валидацию и соблюсти определенные стандарты качества

  • Процесс обновления проще. Еще один небольшой субъективный плюс - практически безболезненные обновления. С начала разработки мы поэтапно обновляли Flutter SDK с версии 3.22 до актуальной 3.32, и за все это время не столкнулись ни с одной критичной проблемой: никакие ключевые модули не ломались, библиотеки поддерживались(кроме HMS PushKit, пришлось удалить deprecated API).
    Для сравнения: в проекте React Native мы так и не смогли корректно перейти выше версии 0.74 с отключенной новой архитектурой.

Если вы стоите перед выбором технологий — не бойтесь перемен. Иногда смена инструмента способна вдохнуть в проект вторую жизнь

Комментарии (30)


  1. MountainGoat
    18.07.2025 17:26

    Эх сравнить бы в лоб этот Flutter с Tauri.


    1. AuToMaton
      18.07.2025 17:26

      Что значит «в лоб»? Даже избранные места, Impeller супротив Canvas, непросто сравнить.

      Чисто теоретически, WebView можно внедрить в Flutter, значит по скорости рендеринга Flutter проиграть не может. Но, если всё будет как обычно, проиграет по объёму и памяти.

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


  1. vsting
    18.07.2025 17:26

    В KMP порог входа существенно выше, чем во Flutter.

    В KMP Куча конфигов повторяющихся, хотя сам по себе язык классный, но экосистема там сильно усложнена, наверное наследие от Java. Грубо говоря отличие только тем, что используется gradle вместо xml. Что бы сделать импорт зависимости ее надо прописать в нескольких местах.


    1. Dancho67
      18.07.2025 17:26

      Есть система сборки Amper, конфиг мало чем от конфига флаттера отличается.


    1. RecodeLiner
      18.07.2025 17:26

      Импорт зависимости происходит лишь раз. Вы скорее всего говорите о применении плагинов, которые уже действительно нужно в коренном и модуле вводить


  1. vsting
    18.07.2025 17:26

    А нельзя во flutter изображение рендерить в фоне сначала, а потом уже готовым выводить на экране?


    1. AlikSondor
      18.07.2025 17:26

      Там проблема не в рендеринге, а в подгрузке из памяти. Упомянутый в статье precacheImage как раз таки и позволяет достать картинку заранее, чтобы она отобразилась мгновенно.


  1. kuftachev
    18.07.2025 17:26

    1. А когда вы сделали миграцию? В React Native где-то погода назад переписали ядро, там сейчас сама архитектура ядра другая и скорость должна была поменяться драматически.

    2. Flutter - это отличная технология, чтобы получать зарплату повыше из-за отсутствия спецов, но мой друг, который так делает, мне рассказывал, что там экосистема вообще пустая, почти ничего нету готового.


    1. AlikSondor
      18.07.2025 17:26

      1. После смены архитектуры РН начал стабильно держать 60 фпс, но не более. Разрыв всё ещё весьма ощутим.

      2. Очень странный у тебя друг. Начиная с того, что зарплаты флаттер разработчиков обычно чуть ниже, чем у нативщиков, и заканчивая огромнейшей экосистемой пакетов под любые задачи на все платформы, доступной в централизованном хранилище pub.dev.


    1. codecity
      18.07.2025 17:26

      там экосистема вообще пустая, почти ничего нету готового

      Вы не попутали с KMP?


    1. I7p9H9
      18.07.2025 17:26

      Кажется вы явно перепутали flutter с чем-то еще. Потому что в п.2 у вас оба утверждения, мягко говоря, странные.
      Может влияет рынок Россия/СНГ, но вот как правило вакансии нативщиков в среднем выше (но это неточно, никаких убедительных доводов кроме личного наблюдения у меня нет).
      Но вот второй постулат точно не про флаттер, там написано чуть больше чем нужно, даже mobX есть на флаттер. Я когда увидел, чуть со стула себя фейспалмом не снес. А потом ничего, втянулся :)


  1. Slonoed
    18.07.2025 17:26

    выйгрышь. Э-хе-хе.


  1. Informatik
    18.07.2025 17:26

    низкая производительность на слабых Android-устройствах

    Сомнительно, что с Flutter сильно лучше будет учитывая, что для комфортной разработки нужны нехилые такие системные ресурсы.


    1. AlikSondor
      18.07.2025 17:26

      Однако с Flutter сильно лучше)))


      1. Informatik
        18.07.2025 17:26

        Я присматриваюсь к кроссплатформенным фреймворкам для своего пет-проекта и Flutter конечно впечатляет своей дружелюбностью к разработчику. Тот же hot reload и пр. Но на моем стареньком железе все работает слишком медленно чтобы что-то разрабатывать. Казалось бы стоит задуматься об апргрейде, но на reddit пишут, что и 32 GB RAM мало. Попробовал поиграть с этим демо-примером на бюджетном Android, тоже подтормаживает. Думаю можно найти что-то более легковесное.


        1. AlikSondor
          18.07.2025 17:26

          На реддите странные люди сидят. Дарт при отладке жрёт пару гигов ОЗУ, не более. Без каких-либо проблем разрабатываю на виндоноуте с 16гб озу. А что именно работает медленно? Просто у нас есть проблемы с анализатором сейчас, которые фиксят. Может то, с чем ты столкнулся, уже поправили.

          Если говорить про пример, то последнее обновление там год назад. Соответственно там ещё не включен impeller, используется Skia. А у скии есть прикол с подтормаживаниями при первом запуске. Флаттер по производительности сейчас буквально лучшее, что есть, не считая полного натива, от которого он редко отстаёт.


  1. AlikSondor
    18.07.2025 17:26

    Ещё с осени Impeller это движок по умолчанию на Android и IOS, какая Skia?


  1. AlikSondor
    18.07.2025 17:26

    И проблема с изображениями решается прекэшем полностью, а не частично

    Если у вас картинки уже сразу после запуска, то достаточно прекэшить пока отображается сплэш запуска. А сами картинки желательно в webp перегнать для минимального размера.


    1. raxenov1 Автор
      18.07.2025 17:26

      Как раз таки частично.

      Если в приложении много ассетов - все кешировать не лучшее решение, посколько это держится в оперативной памяти, приложение может не хило так ресурсов съесть


      1. AlikSondor
        18.07.2025 17:26

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


  1. Kovurr
    18.07.2025 17:26

    А если выбросить оба фреймворка на помойку, будет еще быстрее. Но тогда нужно для IOS искать отдельно разработчиков.


    1. AlikSondor
      18.07.2025 17:26

      На самом деле разница достаточно небольшая. У флаттера шикарный движок рендеринга сейчас.


  1. debug45
    18.07.2025 17:26

    Потратили полгода, заменяя одни костыли другими костылями чуть получше


  1. IlyaSlayer
    18.07.2025 17:26

    А как же MAUI? Существовал в виде Xamarin когда ещё не было мобильной кроссплатформы как такового явления.

    Как по мне, это тот самый гем - про который все забывают


    1. raxenov1 Автор
      18.07.2025 17:26

      В текущее время неактуальный инструмент

      Про поддержку сторонних SDK и библиотек можно забыть


  1. actualinfo
    18.07.2025 17:26

    Как быть, если хочется не только мобильное приложение, но и десктоп? Хочу из вебдева вкатиться в кроссплатформенную разработку и ищу такую серебрянную пулю, чтобы под силу было программисту в одиночку один раз код написать и везде его запустить ))

    React Native вроде бы в десктоп не умеет. Flutter объячил поддержку десктоп, но изначально ориентирован на Windows 10 и новее. А ведь многие до сих пор пользуются Windows 7 на стареньких компах и ноутах.

    Попробовал Tauri, но он требует наличия в системе webview-браузера, что опять же приводит к пляскам с бубном на Win7. А собранный APK для андроида оказался размером 30 Мб - как-то слишком жирно для мелкого приложения. Для сравнения - тестовая APK в Ionic уместилась в приятные 4 Mb. Но Ionic увы не умеет в десктоп :(

    Напрашивается такая полумера - делать мобильную версию в Ionic, а десктопную версию готовить отдельно в NW.JS. На устаревших версиях Windows (даже на WinXP!) NW.JS отлично запускается, если использовать соответсвующую старую версию и придерживаться стандарта ES6.

    Но неожиданно для себя на этой неделе узнал про существование такого фреймворка как Quazar. Пока что не успел его пощупать, но по описанию он позволяет из Vue-приложения сделать мобильное (подтянув автоматом Ionic), десктопное (подтянув автоматом Electron), и даже PWA-версию.

    Правда немного непонятно, как решается вопрос с типичными для десктопа функциями работы с файловой системой и работой с локальной ДБ? Как я понимаю это всё пишется на Node.js. Но ведь в мобилках Node.js не запускается! Получается по любому нужно этот нативный функционал отдельно реализовывать специфичными средствами то ли в Ionic, то ли в RN, то ли во Flutter?


    1. crackedmind
      18.07.2025 17:26

      Потому что в apk попадают so файлы флаттера для нескольких архитектур. Настроив ndk.abiFilters можно сильно уменьшить размер.


    1. AlikSondor
      18.07.2025 17:26

      А у вас ЦА пользуется устаревшими девайсами в основном? Имхо, ради 1% пользователей, нет смысла заморачиваться. Windows 10 вышла 10 с половиной лет назад. Давно уже попереходили все. Для самых отчаянных любителей семёрки можно веб версию запустить, благо флаттер прекрасно это позволяет.


    1. AlikSondor
      18.07.2025 17:26

      Базовый апк флаттера под одну архитектуру — 7-8 мегабайт. С учётом того, что найти девайс, не поддерживающий armv8, сейчас фактически нереально, билдить можно только его. Это базовый размер на движок, дальнейшее расширение веса почти не добавляет. Среднего размера приложение с кучей библиотек у нас в районе 11-12 мегабайт в апк получилось.

      А маркет использует aab, позволяя пользователям загружать файлы только для своей архитектуры, то есть получите то же самое, но даже с поддержкой armv7.

      Рустор в aab тоже умеет, кстати


  1. lil_master
    18.07.2025 17:26

    На мой взгляд, KMP отлично подходит для готовых продуктов, которые уже написаны на Kotlin и хотят добавить поддержку iOS с минимальными затратами.

    В какой то степени вы правы. На самом деле ситуация ещё более плачевная: поддержка iOS - это не только чтобы просто запустилось на Айфоне, это что-то типа: "запустилось на большинстве айфонов и все виды виджетов при скроллинге не лагают". Команда флаттера над этим работает несколько лет и с каждым обновлением фиксит айосовские виджеты. В котлине механизм UI чуток другой, но суть в том что iOS тот же самый. Т е можно убежать от флаттера, но убежать от iOS не получится.

    И вот здесь становится понятной главная фишка флаттера: разработчики жрали волков чтобы на айос хорошо работало и действительно, по сравнению с другими, теперь работает хорошо (субъективно говоря: даже лучше чем родной Swift).

    Поэтому когда говорят о конкуренции в кроссплатформе - претендентов много. Когда начинаешь серьёзно копать в детали - остаётся один флаттер.