Від 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. Її етап комітування може виконуватися синхронно або конкурентно, причому рендерер, заснований на мосту, не мав можливості належним чином підтримувати жоден із цих режимів. Якщо ваша команда хоче отримати таку саму модель конкурентного відображення, яка використовується у веб-версії, 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% краща продуктивність потоку користувацького інтерфейсу для типових додатків, до 3 разів краща — для додатків, які сильно залежать від нативних модулів
- на 20–50% швидші виклики з JavaScript у нативний код
- на 20–40% коротший час запуску, залежно від кількості нативних модулів у додатку та обчислюваних операцій
Більш корисним показником, ніж будь-який відсоток, є те, до яких категорій додатків відбулася зміна з «можливо за допомогою обхідних шляхів» на «пряме використання». Додатки, що інтенсивно використовують камеру, візуалізація даних у реальному часі, складні анімовані інтерфейси та все, що потребує справді синхронних вимірювань макету, або вимагало складних архітектурних рішень, або взагалі не були практичними. У новій архітектурі це звичайні інженерні завдання. Якщо ваш додаток належить до однієї з цих категорій, аргументи на користь міграції є сильними, незалежно від середніх показників; якщо це переважно форми та списки, користь буде реальною, але менш значною. Єдині показники, які мають вплинути на ваше рішення, — це ті, які ви вимірюєте у своєму додатку до та після міграції.
Що виправив нещодавній цикл випусків
Причиною того, що цей рік вважається поворотним моментом, є не нова функціональність, а низка виправлень конкретних проблем, які змушували обережні команди чекати. Для отримання точної інформації перегляньте офіційні примітки до випуску, адже наведений нижче огляд відображає інформацію на момент написання тексту.
- React Native 0.85, випущений у квітні, перебудував систему спільних анімацій та усунув давню причину втрати кадрів. Як повідомляється, переходи з інтенсивними анімаціями на більшості підтримуваних пристроїв зберігають частоту кадрів на рівні 60 fps, замість того щоб погіршуватися під навантаженням.
Міграція на практиці: спочатку аудит залежностей
Це зовсім не означає, що кожен існуючий додаток має переходити вже завтра. Першим показником, який потрібно перевірити перед затвердженням змін, є сумісність бібліотек. На момент написання приблизно 85% популярних пакетів React Native у форматі npm, за даними звітів, підтримують Нову архітектуру, тоді як ще близько 15% підтримують лише мостовий режим або є частково сумісними. Міграції, які зупинилися, майже завжди застрягають саме в цій частині: не у самому фреймворку, а в кількох сторонніх нативних модулях, від яких залежить додаток.
Шаблон, який часто спостерігається у командах, що успішно мігрують, виглядає так:
- Проведіть аудит перед початком роботи. Складіть список усіх нативних залежностей та перевірте їхню сумісність із новою архітектурою за поточною версією, а не за тим, що ви пам’ятаєте з року чи двох тому. Для кожного несумісного пакета вирішіть, чи оновити його, замінити, створити свою копію чи обгорнути його самостійно.
- Використовуйте Codegen. Оскільки Codegen створює типобезпечні нативні інтерфейси на основі ваших специфікацій у TypeScript чи JavaScript, ціла категорія неузгоджень між JavaScript та нативним кодом виявляється під час збирання, а не при запуску програми. Якщо ви пишете власні модулі, наш посібник за посиланням щодо підключення вбудованого Turbo Module кінце-кінцем за допомогою Codegen демонструє повний процес.
Керівництво залежно від ситуації
Початок нового проекту
Немає значних причин уникати цього підходу. Нова архітектура є стандартною, супутні інструменти, зокрема Expo SDK 56, передбачають її використання, а різниця у сумісності зараз настільки мала, що ви навряд чи зіткнетеся з проблемами, якщо тільки у програми немає незвичайних нативних залежностей.
Підтримка програми на старій архітектурі
Почніть з аудиту сумісності, перш ніж приступати до чогось іншого, використовуючи ваш справжній список залежностей. Частка сумісних пакетів постійно зростає, тому оцінка, зроблена рік тому, ймовірно, вже застаріла. Плануйте поетапну міграцію та розглядайте помилки, які виявляє Codegen, як справжню систему безпеки під час переходу, а не як формальність.
Команди, які оцінили його раніше та вирішили почекати
Відкладення на 2023 чи 2024 рік було обґрунтованим рішенням з огляду на наявні тоді проблеми. Відновлення анімацій та виправлення проблем з витоком пам’яті у версії 0.85 саме вирішують проблеми стабільності у продакшені, які були підставою для очікування, тож цей цикл випусків є природним моментом для повторного розгляду цього питання.
Основні висновки
- Стара версія мосту серіалізувала кожен виклик та відкладала його у чергу після зайнятої нитки JavaScript; саме це, а не погана удача, стало причиною багатьох проблем у минулому.
- JSI забезпечує прямі виклики, Fabric переміщує структуру дерева та макет у спільний код на C++ та дозволяє одночасну роботу з React, а TurboModules завантажуються за потреби на основі типованих специфікацій.
- Рівень прискорення залежить від багатьох факторів; приймайте рішення, ґрунтуючись на категорії вашого додатку та власних вимірюваннях.
- Нещодавно змінилися стабільність та інструменти, а не функції: надійність анімацій, поведінка пам’яті протягом тривалих сеансів та швидкість збирання проекту.
- Ризик міграції полягає у сторонніх нативних модулях, тому спочатку перевірте залежності, використовуйте Codegen та поступово переносіть один екран чи модуль за раз.