Дзе ховаецца затрымка у React Native: перакранчванні меж, а не повольны код
Дазвольце дазнаць, чаму шырокаскорасныя API і компаненты з мемаізацыёй можу ўсё ж казацца медленнымі ў React Native, і як батчаванне, тротлінг і дизайн часоў JSI скараць витраты між разнымі версіямі ўсталення.
Крыніцы: The Hidden Latency Cost in React Native Bridge Architectures
Экран React Native можа выглядаць повольныя, нават калі всі показнікі здаюцца нормальнымі: API адпавядае за прыблізна 100 мс, запиты індексуюцца, компаненты зберагаюцца у памяці, а карты запоўнююцца з кэша. Аднак натысканне елемента реагуе занадта дзіўна, нижняя панель працюе з перашкодамі, маркеры на карце не супараднуюцца з рэальнасцю, анімация, якая ў ідеальным тэлефоне працюе без проблем, на бюджэтным Android-прыстрое страблюе, а спраўы з датчыкамі блакуе інтэрфейс. У багатьых такіх случаях вялікі час, які не выкарыстоўваецца, трачаецца там, дзе команды рэдка калі пераглядаюць параметры: падчас перадачы даных між средой JavaScript і натыўным кодам.
У гэтым кялічыку пояснюецца, як працаваў гэты перадачы пад старой сістэмай Bridge, чаму яе выкліканыя расходы нарастаюць, а не праказуюцца ў яком-небудзь адзіном вызове, што на самай працэ практычна змінюе Новая архітектура (JSI, TurboModules, Fabric і перадзеянны рэндерар), а таксама якія прыемы проектавання дапамагаюць залічваць трафік между разнымі средамі выконання дешавым, незалежна ад таго, якая архітектура викорыстоўваецца. Новая архітектура є стандартной з версіі React Native 0.76, але сістэму Bridge все ўсё варта разумець: багато практычных дапрыемаў ўсё яшчэ включаюць старыя модулі, шары взаімадзеяння, старэйшыя бібліятекі і патэрны, створаныя з ўжываннем Bridge. Што яшчэ важнае, адключэнне Bridge не робіць паслаба праектаваныя способы взаінадзеяння между средамі выконання безкоштовнымі.
Две среды выконання ў аднай дапрыеме
Прыгодным ментальным моделюванням є тое, што аплікацыя React Native працюе ў прынеймна двух сэрэгах. Вашы компаненты, статус, правілы роботы та сецесія з інтэрнетам знаходзяцца ў частцы, напісанай на JavaScript; усё, што стосуецца операцыйнай системы, — у натыўной частцы.
┌──────────────────────────────┐
│ JavaScript / TS │
│ │
│ React Components │
│ State Management │
│ Business Logic │
│ API Handling │
└──────────────┬───────────────┘
│
│ JS ↔ Native communication
│
┌──────────────▼───────────────┐
│ Native Side │
│ │
│ Android / iOS APIs │
│ Camera │
│ Bluetooth │
│ GPS │
│ Storage │
│ Native Views │
│ Sensors │
└──────────────────────────────┘
Код React можа працаваць на JavaScript, але практычна кожная значымая функцыя зрэшта-решт патрабуе падтрымкі платформы. Тыповыя прычыны для взаімадзея з платформай включаюць:
- відрасунк натыўных візуальных элементаў інтерфейсу,
- з’явленне камеры,
- чытанне координатаў GPS,
- доступ да системы файлаў,
- актывацыя хаптічных эфектаў,
- взаімадзея з прыстроямі Bluetooth,
- выкарыстоўванне захіщанага зберагача дадзеных,
- выкліканне SDK-ў трэціх сторон,
- прыем дадзеных з сэнсараў,
- выкарыстоўванне іншых API, спецыфічных для платформы.
Таким чынам даны з роўна праходзяць між двумя средавымі. У класычной архітэктуре весь гэты трафік праходзіў через React Native Bridge.
Як легасі-Bridge перадаваў вызывы
У пачатковай разработцы выконанне коду на JavaScript і выконанне на натыўнай сістэме было навмесна роздзелена. Одні разы зв’язку выглядаў працоўна так:
JavaScript
↓
Prepare parameters
↓
Serialize data
↓
Queue bridge message
↓
Cross execution boundary
↓
Native receives message
↓
Native performs operation
↓
Prepare result
↓
Serialize result
↓
Queue response
↓
JavaScript receives result
Ключовая характэрыстыка — ўсё абмены вадзіліся асынхронна. Аргументы серыяваліся, ставіліся ў чергу, перадаваліся через межу, дэсерыяваліся і обрабоўваліся на натыўнай стороне, а будзь-які рэзультат вяртаўся па той жа шлях. Команда React Native у своім дакладзе пра новую архітэктуру назвала серыяванне і асынхронны моставанне ключовымі обмежэннямі старой моделі.
Для разовых вызываў такі надтэрагаты нешкодны. Хорашым прыкладам є запіс строкі у кліпборд.
await Clipboard.setString("Hello");
Калі б не было косвенных расходаў на такі перавод, ніхто з корыстувачаў іх не паведаміць. Проблемы пачынаюцца тады, калі гранічны механізм стае каналам высокай частоты, на які додатак постаўляе неперывны тырот.
Адна вызова — дарога; проблема ў самай модэлі
У дыскусіях пра выконвальную спроможнасць Bridge часта супадзяе, што кожны вызов є надта дарогі. Такое падходжэнне не адображае сутнасці проблемы. Рэальныя сповольнення зазвычай вынікаюць ад сумавання калькольных фактараў:
частота × розмер пакета дадзеных × тырот на планаванне × трэбованні да сінхронізацыі
Праўдзім, што адзін перавод коштае практычна нічога. Тепер узростім колькасць такіх пераводаў:
1 call
10 calls
100 calls
1,000 calls
10,000 calls
Тады трэба зробіць кожны пакет дадзеных большым, выканаць апдэты без перапынку пад час прасування корыстніка, або заставіць выконвацца пераход з Native → JavaScript → Native прытаманнае падчас стварэння наступнага візуальнага стану. Косц, які раней быў незначны сам па сабе, тепер стае часткай критычнага пату стварэння візуальнага контента. Жадна адзінка функцыяў не выглядае повольной у інструментах аналізу; затрымкі распадаюцца між разнымі элементамі системы, і самэ гэта прычына, чаму яны застаюцца непазначальнымі.
Спрыяйце розгляду затрымакаў у взаімадзеі як часткі пату
Дапамагае запісаць кожны этап, праз які праходзіце взаімадзейна:
Total Latency =
JS Work
+ Serialization
+ Queue Waiting
+ Context Boundary Cost
+ Native Work
+ Return Communication
+ Rendering Delay
Команды часта налаштовваюць адзін элемент окрема, зазвычай роботу на рэверсным роўні:
Native Work
альбо роботу на роўні JavaScript:
JS Work
Але корыстнік бачыць толькі суму всіх этах затрымакаў:
TOTAL LATENCY
Разглянем рэверсную операцыю, якая завершаецца за:
2 ms
Сама па сабе гэта чырвоны цячок. Аласць, можа, такі распад:
JS preparation 2 ms
Serialization 2 ms
Queue waiting 4 ms
Native operation 2 ms
Response communication 3 ms
React update 4 ms
--------------------------------
Total 17 ms
„Аператыя 2 мс“ ператворылася на аператыю працы прыблізна 17 мс. У дасведчэннях React Native описваецца бюджет кадра працы апошней 16,67 мс праз 60 FPS; якщо гэты бюджет перакрыцца, кадр можа не быць атрымаваным. Большасць сучасных тэлефонаў аднаавляюць экран чаща, чым 60 Hz, што ўсё больш зменшае гэты бюджет.
Бюджеты кадраў залешаюць мало прастору для додатковых витрачэнняў
Праз 60 Hz арытметыка выглядае так:
1000 ms / 60 ≈ 16.67 ms
Гэта дае прыблізна
16.67 ms
для стварэння кожнага кадра. Праз 120 Hz гэта зменшыцца удвое:
1000 ms / 120 ≈ 8.33 ms
Прыблізна восем мілісекунд — гэта дужа мало. Якщо частка з гэтага часу пайшла на безпраганне перадачу данных туды-сюды между рэнтаймамі, сама архітектура паглотвае бюджет на атрыманне зместу. Сімптамы знаёмы:
- затрымкі анімацый,
У дакументах па прыдатнасці таксама разлічваюць FPS аддзінка JavaScript і FPS аддзінка UI (галоўны). Калі аддзінок JavaScript заповнены, ён не можа шырока працаваць з спрытакамі чыста і ацыянаваць інтэрфейс, які керуецца кодам JavaScript, быстра, тады как взаімадзеянні, якія выконваюцца натыўна, напрыклад прасуванне саборнага спісу, можа застацца плавным. Гэта разлічэнне поясняе класычную проблему пад час дэбагавання: адна частка экрана выглядае плавной, а іншая — застойнаю.
Сцэнарый: стрімаванне месца на карту
Уявіцеся, што аплікацыя для вызову таксі прыме яшчэ ўжо стрім GPS-даных. Простая структура падключэння выглядае так:
Native GPS
↓
Bridge
↓
JavaScript
↓
State Update
↓
React Render
↓
Bridge
↓
Native Map
↓
Marker Position Updated
Тепер запускайце гэты цыкл для кожнага выкаранявання. Ні расчытак пазыцьбы, ні перамешчанне маркера самі по сабе не ўскладненыя, але такая структура прымушае выконваць павтаральныя перадачы даных:
Native → JS → Native
Гэта працюе знову і знову паколькі экран застаецца ачытым. Як толькі паказваецца вяліка колькасць драйвераў, навантажэнне зростае:
Driver 1 position
Driver 2 position
Driver 3 position
Driver 4 position
...
Driver 100 position
Выпадзенне сотні маленькіх апдэйтаў по аднаму можа быць набліжна ў дзесяткі разоў дарожэйша, чым выпадзенне адного, належна падготавленага пакета, які описвае ўсе іх. Асновная прычына — форма прайму даных, а не процес рэндаравання у React.
Сценарый: стрэмы сенсораў
Даныя акцэлераметра і гіроскопа палягаюць на аднай і той жа схеме. Падазроўваюце, што натыўны код выпадзяе:
60 events / second
Кожны запуск праходзіць у JavaScript, там яго обрабоўваюць, і гэта можа спрычыніць новы апдэйт, які вяртаецца назад у натыўны інтэрфейс. Рэзультатам являецца поток даных у рэальны час, які праходзіць через гэтыя межы:
Sensor
↓
Native
↓
JS
↓
Calculation
↓
React
↓
Native UI
Пры низкай частоте падачы званака гэта можа працаваць абсалютна нормальна. Пры высокай частоте, а ўтым болейш на слабейшай апаратуре, званакі можу падаць быстрей, чым прыкладнае програма іх обрабоцуе, што стварае ўжо адну заслоненую вартасць.
Чэргі, якія расту быстрей, чым іх обрабоцуюць
Падазрóў, што званакі падаюць кожны
10 ms
але ўпорацаванне кожнага з іх эфектыўна захопляе
15 ms
Гэта вучынна не проста сповольнена операцыя; гэта застой, які расте без меж. Новыя паказанні падаюць, калі старыя ўсё яшчэ чакаюць на обрабоцу, таму інтэрфейс завжды паказвае мінулы стан. Для рэальна-часовых інтерфейсоў гэта самы лягчы вынік. Корыстніку рэдкасцю трэба кожны проміжны прыклад; яму патрэбна самая няўзабавная значыць, якая мае значэнне. Дизайны, якія адзінаваюць або адхоўляюць застарэлыя званакі, дазволяюць чэрзі застаўаться короткай, а паказанню – актуальным.
Інтэрфейсы для абсаўдных пераказоў падвайнаюць колькасць пераказоў
Гэты прынцып не ўзначае толькі React Native; інжынеры, якія ствараюць распраўленыя системы, сталкнуліся з ім ўжо кожны дзень. Пораўняйце API модуля, які за кожны вызов задае адну палатку:
await nativeModule.setName(name);
await nativeModule.setEmail(email);
await nativeModule.setAge(age);
await nativeModule.setCountry(country);
await nativeModule.setLanguage(language);
з тым, які прыймае весь апдэат за раз:
await nativeModule.updateProfile({
name,
email,
age,
country,
language,
});
Другая версія патрабуе значна меншае колькасць пераказоў для аднаковага рэзультата. Простая правіла гласяць:
Выканайце больш работы за кожны пераказ, і робіце яго рэдкая.
Той самы прынцып застосоўваецца да:
- HTTP API,
- вызоў мікросервісаў,
- доступу да базы дадзенаў,
- рабочых нітэй,
- комунікацыі межа процэсамі,
- мобільных мостоў,
- распраўленых систем у цэлым.
У сетях гэта называецца актыўным абменам данымі. Старыя моставы проста перанеслі гэту класычную проблему распадзеленых систем у ўсё тое ж мобільнае прыкладненне.
Размах пакета — гэта другая вялічына
Частота перадачы даных — толькі палова прычыны; важна таксама і колькість даных, якія перадаюцца. Разглядзім возможнасць перадачы чаго-то подабнага через межу старых тэхналогій:
{
users: [...10000Users]
}
Падаўшы мусі кодаваць структуру, а прымоўшы — яе перабудаваць і обрабляць. Большыя масівы, глэбокая іерархія об’ектаў, метаданы зображэння, бінарныя даны і велікія дрэвы налашчання — усе гэта стварае значны додатковы навантажэння. Найбольш уразлівыя да гэтага навантажэння задачы ўключаюць:
- кадры камеры,
- буферы аудыю,
- обробку зображэння,
- даныя для машыннага навчання,
- велікія рэзультаты запитаў,
- складныя анімазыі,
- жывыя карты,
- стрэмы сенсораў.
Дакументацыя архітектуры React Native выкорыстоўвае крайні, але павучальны прыклад: буферы кадраў камеры у рэальны час, якія можуць запоўняцца дзесяткамі мегабайтаў на кадр і вельмі большыми об’ёмамі па секунде. Павтаральна серыялізаваць такія данні через асінхронны мост проста ўзначытае неправильнае рашэння.
Чаму трубопрацесы камеры робяць выкладкі очывачнымі
Адзін кадр можа мястіць мегабайты пікселей. Тепер неабходна доставляць кадры з
30 FPS
або нават
60 FPS
Якщо кожны з іх павінен быть перакананы ў формат, які падходзіць для JavaScript і можа быць серыялізаваны, і апрацоўваны через асінхронны шар, тады транспорт стае вузькім месцам. Неабходна ухіліцца ад такой ситуацыі для кожнага окремага кадра:
Native Camera
↓
Huge serialized payload
↓
JavaScript
↓
Process
↓
Serialized result
↓
Native
Самэ тым бібліятэкі з высокай працэйнасцю все чыраз больш размешчаюць тэжкія процесы поблізу натыўнай памяці і даюць JavaScript вузкі, эфектыўны інтэрфейс. Гэты падход ведае прычымо да аднаго з найзначнейшых змян у сучасным React Native.
Якія змены ў JSI
JSI (JavaScript Interface) дазволяе JavaScript значна болей безпосередньа взаімадзеяваць з натыўным і C++ кодам. У працоўнасці замест таго, каб абоўсумова павяржваць у спосаб серыявання і абработкі через стары мост:
JavaScript
↓
Serialize
↓
Bridge
↓
Deserialize
↓
Native
Новая архітектура можа выкліквати натыўныя функцыяналы через безпосередні інтэрфейсы:
JavaScript
↓
JSI
↓
C++ / Native
За інформацыяй з дасэбнаў React Native, JSI разрывае JavaScriptу трэсты да об’ектаў на C++ і C++у — трэсты да об’ектаў на JavaScript, таму методы можна вызваць без модэлю серыявання Bridge. Канцэптуальна, вызыванне натыўнага методу пачынае прымаць форму вызывання функцыі на гаспадарскім об’екте, а не падачы звесці да іншага процэсу. Це фундаментальныя змены ў архітэктуре.
Чаму JSI не заканчывае працу над выдатнасцю
Пашырэная неправильная інтарпрэтацыя выглядае так:
"JSI усуняе Bridge, таму проблемы з выдатнасцю зникаюць."
Це не так. Транспорт стаў набліжэней, але паслабленая праектавання системы все раве стварае затрымкі. Сто тысяч сінхронных вызываў через JSI:
100,000 synchronous native calls
Кожны з гэтых спосабаў можа быць дышэўней за вызов Bridge, пры тым большая частка работы як і раней ўсё равно ўсунутая. То ж сама стосуецца падачы вельмі большых об’ектаў знову і знову, выканання роботы, якая сильна навантажае CPU, на потоку JavaScript, блакіравання галоўнага потока UI чы выканання тыяў тысяч компонентаў, якія не патрабаваліся да выканання. Сінхронны доступ адноўу стварае новы рызык: медленны сінхронны вызов натыўнае бібліятэкі тепер блакіруе поток JavaScript безпосередна, замест таго каб быць вырашаным пазней.
Такім чынам, вывад не ў тым, што «Bridge быў медленны, а JSI — быстры». Ён ближэй да гэтага:
Кожны перехад між рантаймамі коштае чагосьці. Структуруйте прыкладнэ програма так, каб непатрэбныя перехады ніколі не выходзілі.
TurboModules: сучасная система натыўных модуляў
TurboModules заменяюць застарэлую систему Native Modules. Старыя модулі былі доступны через Bridge; новыя ж інтегруюцца ўжо тычэй праз JSI і Codegen, які стварае типаваныя зв’язки на адповіднасць спефікацыі. Серед прыямоў, якіе перерахоўвае React Native, ёсць:
- сінхронны доступ да натыўных элементаў, калі гэта неабходна для певнага варыянту выкарыстоўвання,
- безпека типаў па всім межам,
- сумеснае працаванне з C++,
- раздзеленне коду між разнымі платформамі,
- завантажэнне модуляў у спосабе lazy loading.
Завантажэнне у спосабе lazy loading заслуговае на асаблівую увагу. У старой модэлі ініцыялізацыя натыўных модуляў пад час запуску могла прызвесці дадатковую роботу ўсё до таго, як з’явіўся першы экран. Завдзяки lazy loading модуль ініцыялізуецца толькі тады, калі ён упершыне выкарыстоўваецца. У працоўным режыме:
Application starts
↓
Initialize everything
↓
Eventually show UI
процес запуску можа выглядаць так:
Application starts
↓
Initialize required components
↓
Show UI
↓
Load additional modules when needed
Такі змяны вплываюць на тое, што корыстувальнікі бачаюць значна больш, чым просто цифры тэстаў: на шточнае час аплікацыя стае гатовай да вжывання.
Fabric: аспект адрасавання пад час перадзялу
Слою камунікацыі не была ўсёй елементам, які быў перабудаваны. Fabric, новая система адрасавання, пераносіць большую частку інфраструктуры адрасавання ў спільны C++ і дазваляе выконваць функцыі, якія былі складныя пад старым асінхронным модэлем, у тым числе:
- сінхронны доступ да лейауту там, дзе гэта неабходна,
- лепшая взаімадзейнасць,
- калькі прыоритэта апдэйтаў,
- падтрымка сучасных функцый React,
- больш гнеклівае планаванне.
Прыоритэты маюць значэнне, таму што інтарфейсы не ставяцца да кожнага апдэйта однакова:
User presses button → HIGH priority
Background data refresh → LOWER priority
Analytics event → LOW priority
Animation → HIGH priority
Prefetching → LOW priority
Спроможны рэндэрары не должны спрыяць адпрацоўкі нажатня на кнопку і запыту аналітыки як чагось наоднакова срочнага. Прыорітэзаванне ёсць аднай з базоў реактыўнага інтерфейсу, а Fabric дазволяе React Native браты участь у модэлі планавання React. Якщо вы хочаце пазнакоміцца з тым, як сам React кодуе прыорітэт апдэйтаў, статыя на як React лейны выкарыстоўваюць бітмаскі для кодавання прыорітэта распаводзіцься пра гэты механізм.
Прычынай проблем ёсць часта зміна граніц
Якбы весья проблема мусела быць выражаная двумя словамі, імі б былі часта зміна граніц: пастойчыя перадачы маленькіх фрагментаў інфармацыі туды-сюды межу выконавачых средаў.
JS
↓
Native
↓
JS
↓
Native
↓
JS
↓
Native
Калі вы бачыце такую структуру, гэта зазвычай значыць, што адпаведна адпаведальнасць за певную функцыю была раздзелена не там, дзе трэба. Пытанне, якое трэба задаць, — гдэ на самай праце павінна знаходзіцца. Возьмемо обробку аудыя. Дизайн, які супакоўваеся частымі змянамі, выглядае так:
Native microphone
→ JS audio frame
→ calculate
→ native
→ next frame
→ JS
→ calculate
→ native
Кращы спосаб раздзелу парадку ў тым, каб обробка высокачастотных сигналаў вадзілася натыўнаму коду, а JavaScript прымалі толькі тыя рэзультаты, якія варта выкарыстоўваць:
Native microphone
↓
Native processing
↓
Native processing
↓
Native processing
↓
Meaningful result
↓
JavaScript
Колькасць вычыслаў можа быць падобная, але шаблон комунікацыі ўсё ж разны, як і навантажэння на оба поты.
Практычныя методы для эфективнейшай роботы
Аб’еднанне операцый у адны вызов
Цыкл, який чакае на натыўны вызов для кожнага элемента, ёсць частым выкліканнем проблем супакоўвання:
for (const item of items) {
await NativeModule.saveItem(item);
}
Калі є 500 элементаў, гэта можа прымусіць да сотняў окремых ператракаванняў, кожна з якіх мае свою додатковую вартасць. Кращы натыўны API прымае всю колекцію і дозволяе натыўнаму коду выканаць ітерацію:
await NativeModule.saveItems(items);
Разлік у форме легка апісаць:
❌ 500 boundary crossings × small operation
✅ 1 boundary crossing × 500 native operations
Групаванне ўскладненняў ёсць адным з простейшых і найэфектывнейшых архітектурных оптымізацый. Адна з деталей дизайну, якую трэба узяць пад увагу: калі выканана групаваная запытка, неабходна чыстая схема абработкі памылак, напрыклад, інформацыя пра тые элементы, якія не запрацавалі, ўбліжча, каб адна памылка не прывела да безследнага абрабатавання рэшты.
Скорачыце частоту высокачастотных запыткаў
Падазроумеце, што натыўны модуль выдае 120 апдэйтаў за секунду. JavaScript рэдка патрабуе ўсіх іх. Для багацькоў інтэрфейсаў апдэйты ў колькасці 20 за секунду на экране выглядаюць практычна аднакова. У замест на тое, каб абрабатваць
120 events / second
вы можете ператракаваць
20 events / second
альбо скарабатваць запісы наявнае калітку і перадаваць толькі найсвежэйшы стан. Выкарыстоўванне наявнае калітку для контролю частоты запісоў зазвычай краща, чым выкарыстоўванне JavaScript, таму што запісы, якія былі адхілены ў JavaScript, вже заплатілі за перавод дадзеных. Гэты падход падходзіць для:
- трынкування месцазнаходжэння,
- запісоў прасування,
- дадзеных сэнсараў,
- апавешчэнняя пра прыбыль,
- рывоў аудыя,
- статыстык сеті,
- ранення на карце,
- жывых панелей управління.
Найдешавшы запіс — гэты, які ніколі не адправляецца.
Перадавайце значэнне, а не сырыя дадзеныя
Уместа таго, каб адправляць ўсё гэта
10 MB raw dataset
з наявнае калітку ў JavaScript і прасіць JavaScript аб вычысленні адпаведзення, вычысляйце адпаведзенне там, дзе вучынкі вяліся. Заменіце гэты прабег:
Native
↓
10 MB data
↓
JavaScript
↓
find average
на гэты:
Native
↓
calculate average
↓
JavaScript receives: 42.7
Корача кажучы, перадавайце
meaning
заместо
raw information
колікве архітектура гэта дазваляе. Недзеяга ў тым, што логіка пераходзіць у натыўны код, які неабходна пісаць і тэставаць для кожнай платформы або ў спільным C++; выкорыстоўваць гэта толькі для дадзеных, якія справаўна великія або выкарыстоўваюцца часта.
Затрымка сеті — гэта не затрымка взаімадзеяння
Шырокаспектны бэкенд можа замаскуваць медленную роботу. Скажымо, API адпавядае за
120 ms
і панель монітарынгу зелёная. Аднак пасля таго, як прыходзіць адпаведзь, ўсё равно трэба здарыцца багато чаго:
API Response
↓
JSON Parsing
↓
State Transformation
↓
Redux / Zustand / Context
↓
Component Re-rendering
↓
JS ↔ Native interaction
↓
Layout
↓
UI Commit
Калі рэзультат з’являецца на экране, корыстнік можа вучакаць
200+ ms
Звярненне на бэкэнд паказвае, што все працуе быстро; адміністраторы праграмы скаржаюцца, што ўсё сповольна. Абодве стороны кажуць правду, таму што вони меряюць разныя параметры. Звярненне на бэкэнд і звярненне пад час взаімадзеі — это разныя показнікі, і для мобільных дапрацоўак самэй важнай ёст той, што вплывае на адчування корыстніка ўжо пад час взаімадзеі.
Тэставанне на прыстроях, якія фактычна выкарыстоўваюць вашы корыстнікі
Проблемы з працэздатнасцю любяя хавацца на апаратнай частцы разрабоў. Флагманскі тэлефон мае быстрый CPU, достатню колькасць памяці, шырокую память зберагача, моцную GPU і хорашыя можлівасці для ахалоджэння, таму все выглядае нормальна. Аднак корыстнікі, якія выкарыстоўваюць дапрацоўку ў рэальных умовах, запускаюць тую ж версію на тэлефонах з значна меншыми ресурсамі, і
small inefficiency × thousands of operations
проблемы раптам стаюць виднымі. Тэставанне толькі на преміум-прыстроях є рызыкованым. Адлічальная матрыца прыстроў мусі включаць прынеймна:
High-end device
Mid-range device
Lower-end supported device
Гэта мае вялікое значэнне, калі ваша аудытарыя складаецца з рынкаў, дзе большая частка корыстувачаў вжывае доступныя тэлфоны на базе Android.
Мераваць трэба версіі для выпуску, а не версіі разработкі
Версіі разработкі виконваюць дадатковыя задачі, зв’язаныя з паведамленнямі пра проблемы, налагоджэнням і інструментамі, і офіцыйныя рэкамендацыі па працоўнасці чынна ўпораць на мераванне з версіямі для выпуску. Перш чым вырашаць, што сам React Native є повольным, спачатку прааналізуйце
Release / Production build
замест таго, каб паслужыцца толькі
Development build
Інакш вы можете пасвятіць гады на ліквідацыю сповольнення, якога ніхто з корыстувачаў ніколі не зазнае.
Чэрніцкі список для дыягностикі повольнага экрана
Вжыванне функцыі useMemo() у проблемнай частцы экрана рэдкая разваўляецца як правільны першы крок. Усё-такі спачатку розгляньце этыя пытанні.
1. Чы ўсё ў порядку з потам JavaScript?
Шукайце:
- дорогія цыклы,
2. Чы робіцца занятым поток UI?
Шукайце:
- дорогія адрадзіцельніцы на роўным канале,
- складныя макеты,
- занадта вялікія зображэння,
- глыбокія іерархіі відображэння,
- анімацыі, якія запускаюцца на неправильным потоку.
3. Чы занадто часта перакрываецца межа роўнага каналу?
Шукайце павтаральныя запускі такога типу, ў чымасці высокай частоты:
Native → JS → Native
4. Якія вельмі большыя пакеты дадзейна?
Лічэнне вызоваў недастатня. Змерыце, сколькі дадзейна праходзіць через кожны з іх.
5. Можна ліквідаваць вызывы пакетамі?
Як толькі можна, ператворыце
many small operations
на
one meaningful operation
6. Чы даклэгаеся JavaScriptу гэтыя данні?
Якщо натыўны код стварае 1 000 пунктавых дадзенняў, а JavaScript выкарыстоўвае толькі іх сярэжню, то неабходна адправіць самую сярэжню.
7. Чы хаця бы трэба гэта зрабіць зараз?
Зберагчы
User-critical work
за выняткам
background work
Аналітыка ніколі не должна конкуруваць з фідбекам ад супакоўкі ў тым жа кадре.
8. Чы ўсё яшчэ існуе залежнасць ад старой архітектуры?
Настаўныя дакументы React Native пазначаюць Legacy Native Modules і Legacy Native Components як застарэлыя і рекамендуюць выкарыстоўваць Turbo Native Modules і Fabric Native Components, калі гэта дапаможна. Калі вы выбіраеце пакет для функцыяй, чутлівых да выдатнасці, пераканайцеся, якую архітектуру ён падтрымляе; бібліятэка, якая працуе через шар інтэроперабельнасці, можа не даць апэктыўных рызыку. Статус падтрымкі быстра зменяецца, таму пераканайцеся ў гэтым, адзірнуўшысь у настаўныя прыметкі выдання бібліятэкі.
Правіло параджання задач
Для будзь-якай функцыі, чутлівай да выкарыстоўвання ресурсаў, спачатку трэба выявіць, якая частка прыложэння на самай адзе ўжо відпавядае за её выкананне.
- Калі задача належыць да JavaScript (правіла бізнесу, стан UI, оркестрацыя), нехай яна застаецца там і утримваюцца ад викорыстоўвання натіўнага коду на кожны крок.
- Калі задача належыць да натіўнага коду (ціклы сенсараў, обробка кадраў, буферы адгуку), нехай гэтыя крокі выканаваліся натыўна і застаўаліся там.
- Перакрыць межу трэба тады, калі ёсць значныя данні для перадачы, а не тады, калі кожны проміжны крок павінен быць видны ў обох частках.
Здаровы падзел зазвычай выглядае так:
┌───────────────────────┐
│ JavaScript │
│ │
│ Business decisions │
│ Application state │
│ UI orchestration │
└───────────┬───────────┘
│
Meaningful calls
│
┌───────────▼───────────┐
│ Native │
│ │
│ Platform APIs │
│ High-frequency work │
│ Hardware interaction │
└───────────────────────┘
а не так, сотні разоў за секунду:
JS ↔ Native ↔ JS ↔ Native ↔ JS ↔ Native
Што насправды выправляе новая архітектура
Новая архітектура усувае калькольныя структурныя абмежэнні старага модэлю. У змену таму, што ўсё будавалася навакол обавязковага, серыяванага, асінхроннага «моста», сучасны React Native забезпечвае прымітнае вяроўванне через JSI:
JSI
↓
Direct JS/native interfacing
разам з іншымі элементамі, якія функцыонуюць на його аднойчыне:
TurboModules
↓
Modern native module system Fabric
↓
Modern rendering architecture Codegen
↓
Typed interfaces between environments Modern scheduling
↓
Better prioritization of work
Команда React Native адзначае, што усуненне «моста» скарочвае навантажэння, зв’язанае з серыяваннем, паспрыява шырэйшам запуску і дазволяе болей прымітную комунікацыю межа JavaScript і натыўным рантаймам. Глэбэйшая змяна — гнучкасць. Фреймворк тепер мае большы контроль над:
- часам выканання задач,
- месцем выканання задач,
- способам доступу да натыўных функцый,
- тымі апдэйтамі, якія маюць прыоритет.
Гэты урок прыменяецца да кожных меж
Якща адаптавацься да React Native, то патэрн застаецца адным і тым жа: кожная архітэктурная межа мае свойя наследкі. Паглядзіце на гэтыя пераходы:
Browser → API
Service → Service
Application → Database
Process → Worker
JavaScript → Native
Application → Redis
Backend → Message Broker
Сістэмы часта проектуюцца так, нібы вызовы за межамі гэтых ліній працююць як звычныя вызовы функцый унутрэй процеса. Але так не ўсё. Кожная межа додае розныя элементы:
- серіяваванне,
- копіюванне,
- планаванне,
- запіс у чергу,
- меры дзеяння ў сеті,
- сінхронізацыя,
- змена контэксту,
- обраОаванне памяці ад бягучых проблем,
- кантроль памяці.
Bridge проста зробіў гэты прынцып незвычайна відчутным унутрэй дзяўальніка для телефона.
Спачатку пасправціваеце архітэктуру, а потым мікрооптымацыі
Існуе спакуса адразу вжываць такія інструменты:
useMemo(...)
useCallback(...)
React.memo(...)
У іх є рэальныя застосоўкі. Але якщо прыложэнне перадае 5 МБ непатрэбных дадзеных між раундамі выканання 30 разоў на секунду, то заўсёды зменшэнне часу выканання на 0,2 мс за дапамогою useMemo() не будзе значным. Работа над аддачай прыладкі ёсць наўярожнейшая, калі яна ведаецца за такой парад:
Architecture
↓
Data flow
↓
Scheduling
↓
Rendering
↓
Algorithms
↓
Micro-optimizations
а не наадворсу.
Ключовыя выводы
Система Bridge не была простаю паскудным інжынерным рашэнням. Яна дазволяла JavaScript і натыўным кодам заставацца адсакрэннымі пад час асінхронной камунікацыі, і значна спрыяла развіццю React Native. Калі дапыткі сталаўся ўжо з рэальнай часу перадачай паведамленняў, рознымі анімацыямі, відэокамерамі, тэхнологіямі машыннага навучання, картамі, обробкай аудыю, складными жестамі і іншымі форматамі высокачастотной натыўнай інтэрацыі, межы серыяванага асінхроннага камунікавання сталаўся лёгка дасягнучымі. JSI, TurboModules, Fabric і перадзеяныя модэлі планавання і адрасавання усунулі багато з гэтых меж, але не самую сутнасць проблемы.
- Меркуйце весь процес інтэрацыі, а не толькі вызов натыўнага коду чы адпаведны адказ API.
- Расраховвайце час на адрасавання пакета дадзеных: пры 60 Hz — апошней 16,67 мс, а пры 120 Hz — апошней 8,33 мс.
- Агрупавайце операцыі, контролюйце частоту выдачы паведамленняў у самым джэреле і адправляйце результаты заместо сырых дадзеных.
Калі праграма выглядае медленной, нават якшо кожны елемент працюе быстра, паглядзіце на разлікі межы гэтымі елементамі. Часта дорогі ў часе элемент — не сама робота, а вялічына шляху, які павінны працягнуць дадзенняя, каб да яго дасягнуць.