От Bridge до JSI: как определить момент для миграции на новую архитектуру React Native
Как JSI, Fabric и TurboModules заменяют старый мост React Native, что на практике означают полученные преимущества, и как безопасно провести аудит и миграцию существующего приложения.
Источники: React Native’s New Architecture Finally Stopped Being a Compromise
Любой, кто раньше развертывал сложные приложения на React Native с использованием устаревшей архитектуры, сталкивался с замедлением прокрутки в момент, когда поток JavaScript занят другими задачами — например, обработкой ответа с сети или передачей изменений состояния через большое дерево компонентов. Такие сбои были не результатом невезения, а прямым следствием способа работы старого моста связи. Новая архитектура, заменившая его, является стандартной с версии React Native 0.76, но долгое время многие команды откладывали её внедрение на потом. В этой статье объясняется, что именно меняется в трёх составляющих Новой архитектуры, как интерпретировать заявления об её производительности и как решить, стоит ли мигрировать существующее приложение и как это сделать.
Почему старый мост связи вызывал сбои
В архитектуре устаревшей версии каждое взаимодействие между JavaScript и нативным кодом происходило через асинхронный мост. Запрос сериализовывался в JSON, помещался в очередь на мосту, десериализовывался на стороне нативного кода и выполнялся там, причем результаты возвращались обратно по тому же пути. Из такой конструкции следуют два вывода: каждое взаимодействие сопровождалось затратами на сериализацию, и поскольку мост был асинхронным и упорядоченным, любой запрос к нативному коду вынужден был ждать, пока не освободится поток JavaScript. В результате занятый поток JavaScript блокировал прокрутку, форматирование и действия, зависящие от него.
Эта замена существует в той или иной форме уже много лет, и с версии 0.76 она официально стала стандартной. Однако быть стандартным вариантом и тем, на чём будет основываться производственное приложение у осторожной команды, — это разные вещи. Выпуски в первой половине этого года, React Native 0.85, а затем Expo SDK 56, широко считаются моментом, когда эта пробела была преодолена; подробности рассматриваются далее.
Три компонента новой архитектуры
Если не учитывать маркетинг, новая архитектура состоит из трёх взаимодействующих компонентов, каждый из которых заменяет определённую часть «моста».
JSI: прямые вызовы вместо сообщений
Интерфейс JavaScript (JSI) является основой. Это легкий интерфейс на C++, с помощью которого JavaScript может хранить ссылки на нативные объекты и напрямую вызывать нативные функции, включая синхронные вызовы там, где это целесообразно. Нет сериализации в формате JSON, нет очереди моста и нет асинхронных обратных вызовов для тех запросов, которые их не требуют. Старый процесс сериализации, размещения в очередь, десериализации и направления запроса сводится по сути к обычному вызову функции на C++.
Fabric: общее теневое дерево на C++
Fabric — это система отрисовки, построенная на JSI. Она организует процесс отрисовки в три слоя: дерево React Fiber на JavaScript, «тень» — дополнительное дерево данных и дерево нативных элементов интерфейса. Ключевое отличие заключается в том, что «тень» теперь находится в общем коде на C++, а не передается через мост из JavaScript. Расположение элементов с использованием Yoga вычисляется именно в этом общем слое, поэтому данные о расположении больше не передаются через мост.
Fabric также является основой, позволяющей функциям React с параллельной обработкой, таким как анимации переходов, механизм Suspense и функция useTransition, работать в React Native. Его этап выполнения может происходить синхронно или параллельно, а рендерер, основанный на мосте, не мог корректно поддерживать ни один из этих режимов. Если ваша команда хочет использовать ту же модель параллельной отрисовки, что и в веб-версии React, Fabric является необходимым условием, а не просто оптимизацией.
TurboModules: ленивые, типизированные нативные модули
TurboModules заменяют устаревшую систему NativeModules для доступа к функционалу нативного кода. Приведённый фрагмент сравнивает эти два подхода. Старый способ импорта загружает модуль из NativeModules, который инициализировался сразу при запуске, что приводило к расходам на сериализацию при каждом вызове. Новый способ импорта загружает модуль, сгенерированный на основе типизированного описания с использованием JSI; он инициализируется только при первом использовании и может вызываться синхронно там, где это целесообразно.
// Old NativeModules: loaded eagerly at startup, whether used or not,
// with every call paying the bridge's serialization cost
import { NativeModules } from 'react-native';
const { CameraModule } = NativeModules;
// TurboModules: JSI-backed, lazily loaded only when actually called,
// with type-safe interfaces generated by Codegen from your spec
import CameraModule from './specs/NativeCameraModule';
CameraModule.capturePhoto(); // direct, synchronous-capable call via JSI
Подход с отложенной загрузкой имеет большее значение, чем кажется на первый взгляд. В старых системах приложение с большим количеством нативных модулей тратило ресурсы на их инициализацию при запуске для каждого из них, даже если в течение сессии с ними почти не работали. TurboModules оплачиваются только тогда, когда модуль действительно используется. Второй важный аспект — это типизированные спецификации: Codegen преобразует их в нативные интерфейсы, что подробно рассматривается в нашей статье о том, почему Codegen делает использование спецификаций обязательным для TurboModules.
Аккуратное чтение заявлений об производительности
Цифры, приводимые для новой архитектуры, различаются в зависимости от источника и нагрузки, поэтому следует рассматривать их как ориентир, а не точные показатели, которые можно обещать заинтересованным сторонам. Среди сообщаемых результатов:
- примерно на 10–30% лучшая производительность потока UI для типичных приложений, до 3 раз выше — для приложений, сильно зависящих от нативных модулей
- на 20–50% быстрее вызовы из JavaScript в нативный код
- на 20–40% меньше времени запуска, в зависимости от количества нативных модулей в приложении и от того, какие операции измеряются
Более полезным показателем, чем любой процент, является информация о том, какие категории приложений перешли из разряда «возможны с использованием обходных решений» в разряд «работают без проблем». Приложения, требующие активного использования камеры, визуализация данных в реальном времени, сложные анимированные интерфейсы, а также любые приложения, которым необходимо точное синхронное измерение макета, либо требовали сложных архитектурных решений, либо вообще не были практичными. В рамках новой архитектуры это становится обычной инженерной задачей. Если ваше приложение относится к одной из этих категорий, аргументы в пользу миграции очень сильны, независимо от средних показателей; если же оно в основном использует формы и списки, преимущества будут реальными, но более скромными. Единственные показатели, которые должны влиять на ваше решение, — это те, которые вы сами измеряете в своем приложении до и после миграции.
Что было исправлено в последнем цикле выпусков
Причина, по которой этот год считается поворотным моментом, заключается не в появлении новых функций, а в наборе исправлений конкретных проблем, которые заставляли осторожные команды ждать. Для получения точной информации ознакомьтесь с официальными записями об обновлении, поскольку приведенное ниже краткое описание основано на отчетах на момент написания текста.
- React Native 0.85, выпущенный в апреле, пересоздал систему общих анимаций и устранил давно существовавшую причину потери кадров. Сообщается, что переходы с активными анимациями теперь работают со скоростью 60 кадров в секунду на большинстве поддерживаемых устройств, без снижения производительности при нагрузке.
Миграция на практике: сначала аудит зависимостей
Все это не означает, что каждое существующее приложение должно мигрировать уже завтра. Главным показателем перед тем, как что-то изменить, является совместимость библиотек. На момент написания статьи около 85% популярных npm-пакетов React Native, по имеющимся данным, поддерживают Новую архитектуру, тогда как примерно 15% работают только в режиме моста или имеют частичную совместимость. Миграции, застопорившиеся на этапе, почти всегда связаны именно с этими оставшимися пакетами: не с самой фреймворковой структурой, а с некоторыми сторонними нативными модулями, от которых зависит приложение.
Шаблон, характерный для команд, которые успешно мигрируют, выглядит следующим образом:
- Проведите аудит перед началом работы. Составьте список всех нативных зависимостей и проверьте их совместимость с новой архитектурой на основе текущей версии, а не той, которую вы помните с прошлого года или двух. Для каждой несовместимой пакетной библиотеки решите, следует ли её обновить, заменить, скопировать или самостоятельно переработать.
- Используйте Codegen. Поскольку Codegen генерирует типобезопасные нативные интерфейсы на основе ваших спецификаций на TypeScript или JavaScript, целый класс несоответствий между JavaScript и нативным кодом обнаруживается на этапе сборки, вместо того чтобы приводить к сбоям во время выполнения. Если вы пишете собственные модули, наш пошаговый гайд по подключению внутриприложенного Turbo Module с использованием Codegen показывает весь процесс.
Рекомендации в зависимости от ситуации
Начало нового проекта
Практически нет причин отказываться от использования новой архитектуры. Она является стандартной, сопутствующие инструменты, в частности Expo SDK 56, предполагают её использование, а разрыв в совместимости сейчас настолько мал, что вы вряд ли столкнётесь с проблемами, если только у приложения нет необычных нативных зависимостей.
Обслуживание приложения с использованием устаревшей архитектуры
Прежде чем что-либо менять, начните с аудита совместимости, используя ваш реальный список зависимостей. Доля совместимых пакетов постоянно растет, поэтому оценка, сделанная год назад, скорее всего устарела. Планируйте поэтапную миграцию и рассматривайте ошибки, обнаруживаемые Codegen во время перехода, как настоящую защиту, а не просто формальность.
Команды, которые оценили его ранее и решили подождать
Отсрочка на 2023 или 2024 год была разумным решением учитывая наличие недостатков в то время. Исправления проблем с пересозданием анимаций и утечками памяти в версии 0.85 направлены именно на проблемы стабильности в производственных условиях, которые и послужили причиной отсрочки, поэтому сейчас — подходящее время для повторного рассмотрения этого вопроса.
Основные выводы
- Устаревший мост сериализовал каждый вызов и ставил его в очередь после занятой нити JavaScript; именно поэтому, а не из-за плохой удачи, возникали многие проблемы в прошлом.
- JSI обеспечивает прямые вызовы, Fabric перемещает дерево теней и макет в общедоступный код на C++, что позволяет использовать React одновременно с другими компонентами, а TurboModules загружаются по мере необходимости на основе типизированных спецификаций.
- Разница в скорости работы после обновлений сильно варьируется; принимайте решение, исходя из категории вашего приложения и собственных измерений.
- Недавние изменения касаются стабильности и инструментов, а не новых функций: надежности анимаций, поведения памяти в течение длительных сессий и скорости сборки.
- Риск миграции связан с сторонними нативными модулями, поэтому сначала проверьте зависимости, используйте Codegen и постепенно переносите по одному экрану или модулю за раз.