Когда мы только начинали разработку мобильного приложения, выбор пал на 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, реализован оффлайн-режим, активная работа с камерой, а также использование нативных модулей.
Каков результат? Что мы получили?
Стабильность
У собственного движка отрисовки есть свои плюсы - это будет выглядеть и работать одинаково на всех платформах, с багами или без.
Ранее, при использовании React Native, мы сталкивались с ситуациями, когда на определенных устройствах(Samsung, Oppo, Honor) происходили визуальные баги - часть контента не отображалась, но обработчики продолжали отрабатывать.
После закрытия камеры контент предыдущий страницы исчез Часть BottomSheet не видно Производительность
Средний 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мб React Native - потребление в пике 545мб Нюансы
Безусловно, Flutter мощный инструмент, но и у него есть свои проблемы, с которыми мы продолжаем жить.
Отрисовка изображений
Эта проблема существует с самого появления фреймворка и до сих пор вызывает дискомфорт. Из-за особенностей отрисовки ресурсов изображение не появляется сразу:
На карте, например, иконки через MapKit SDK могут не успеть отрисоваться вовремя — вместо маркеров пользователь видит “пустой” круг
На первом экране онбординга в течение первых 100-200 мс вместо фоновой картинки пользователь видит белый экран, что особенно раздражает.
Частично это фиксится с помощью функции
precacheImage
, подгружая изображения заранее. Но согласитесь, если вы впервые запускаете приложение и видите "белое полотно" вместо фона - впечатление будет испорчено.
Фризы при первых анимациях
Первое с чем мы столкнулись при работе с 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.Обработка 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 с отключенной новой архитектурой.
Если вы стоите перед выбором технологий — не бойтесь перемен. Иногда смена инструмента способна вдохнуть в проект вторую жизнь.