Главная / Статьи

От Bridge до JSI: как определить момент для миграции на новую архитектуру React Native

Как JSI, Fabric и TurboModules заменяют старый мост React Native, что на практике означают полученные преимущества, и как безопасно провести аудит и миграцию существующего приложения.

Издатель: REACTAPP.TOP8 мин чтения1869 слов

Источники: 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 кадров в секунду на большинстве поддерживаемых устройств, без снижения производительности при нагрузке.
  • В этом же обновлении была устранена значительная группа утечек памяти как в Android, так и в iOS, из-за которых длительные сессии использования приложения постепенно становились медленнее. Такое замедление проявляется только после длительной работы приложения, что затрудняет его обнаружение в рамках стандартных тестов качества, а также диагностику на основе отчетов пользователей о постепенном изменении его поведения.
  • Expo SDK 56, выпущенный в мае, предположительно сокращает время сборки локальных версий средних по размеру приложений с 110 до 120 секунд до менее чем 50 секунд, а также уменьшает время публикации обновлений через беспроводную связь примерно на треть. Для команд, выпускающих приложения ежедневно, эти экономии накапливаются быстро.
  • Интеграция с React 19.2 приносит преимущества командам, использующим React Native Web или гибридные приложения. Обновления функций Suspense с серверной стороны и Web Streams позволяют раньше передавать больший объем данных интерфейса, снижая блокировку основного потока, что способствует улучшению работы веб-части экосистемы, которую в пресс-релизах часто недооценивают.
  • Миграция на практике: сначала аудит зависимостей

    Все это не означает, что каждое существующее приложение должно мигрировать уже завтра. Главным показателем перед тем, как что-то изменить, является совместимость библиотек. На момент написания статьи около 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 и постепенно переносите по одному экрану или модулю за раз.