Ограничения на производителността на React Native и как да ги преодолеем

не укротявай дракона, язди го

React Native има голямо обещание по отношение на отличното изживяване на разработчици с Javascript и повторно използване на код между платформите. Големият въпрос е - идват ли тези ползи с цената на изпълнението? Колко добре може React Native да се задържи срещу чисто местни реализации? Оказва се, че доста добре. Особено, ако разбирате пътя си около ограниченията, присъщи на рамката.

Реагирайте Native под капака

За да разберем ограниченията на производителността на React Native, първо трябва да разберем вътрешната му работа. За по-голяма яснота ще се опитам да запазя тази част на високо ниво. Ако търсите изисканите детайли, вижте този отличен пост от Тадеу Загало.

Едно от основните предпоставки на React Native е предоставянето на естествено изживяване на приложението, което очакваха мобилните потребители - като се съсредоточи върху ефективността на разработчиците, базирани на Javascript и React. С други думи, въпреки че по-голямата част от кода на приложението ни е написана на Javascript, самият потребителски интерфейс на нашето приложение е напълно роден.

Това означава, че нашето приложение работи в две различни области:

  • Родното царство - Царството на Objective-C / Swift в iOS и Java в Android. Това е мястото, където ние взаимодействаме със ОС и където се показват всички изгледи. UI се манипулира изключително върху основната нишка, но може да има и други за изчисляване на фона. React Native прави повечето от тежките повдигания в тази сфера за нас.
  • Сферата JS - Javascript се изпълнява в отделна тема от Javascript-двигател. Нашата бизнес логика, включително кои изгледи за показване и как да ги стилизираме, обикновено се прилага тук.

Променливите, дефинирани в едната област, не могат да бъдат пряко достъпни в другата. Това означава, че цялата комуникация между двете области трябва да се извършва изрично през мост. Това всъщност е подобно на концепцията как клиентите и сървърите комуникират по мрежата - данните трябва да бъдат сериализирани, за да преминат. Страхотен анекдот - когато отстраните грешката си от RN JS код в Chrome, двете сфери действително работят на различни компютри (вашия десктоп и мобилен телефон) и мостът между тях преминава през WebSocket.

Тук се крие един от основните ключове за разбирането на изпълнението на React Native. Всяко царство само по себе си пламтящо бързо. Тесното място на производителността често се случва, когато преминаваме от едната област в другата. За да архитектираме приложенията React Native, трябва да задържим пропуските през моста до минимум.

React, с концепцията си за виртуален DOM, ни осигурява отлична оптимизация извън опаковката. Промените в нашите представени компоненти в JS се групират асинхронно с интелигентен диференциален алгоритъм - по този начин се намалява до минимум количеството данни, изпратени през моста. Това всъщност е причината React Native да е по-ефективна от конкурентните технологии като Appcelerator, които са я предшествали преди няколко години.

Игра с случай на реално използване

Виждате бързата карта вляво? Това е популярен мобилен UX модел, използван в приложения като Google Now.

Освен това е изненадващо интересно за изпълнение, съобразно представянето.

Ще прилагаме този пример многократно и ще видим последиците от изпълнението на всеки подход.

Има смисъл да се създаде компонент за многократна употреба Swipeable, който добавя поведението на пръст (x превод и промяна на непрозрачността) към всеки компонент на съдържанието, който в случая даваме като дъщерна карта.

Първата ни реализация - PanResponder

Нека започнем с директния подход. Тъй като искаме да слушаме жестове на допир, ще използваме PanResponder на React Native. Всеки път, когато получим събитие за преместване, ще изчислим новата непрозрачност и x превод въз основа на общото изминато хоризонтално разстояние и ще ги актуализираме, като използваме местно състояние:

Какво представяне трябва да очакваме от този подход? Нека си припомним указанията, посочени по-рано - За да създадем архитект на React Native приложения, трябва да поддържаме преминаванията през моста до минимум.

Изглежда, че тази примерна реализация прави точно обратното. Събитията на допир произхождат от родния свят, тъй като там устройството проследява пръста на потребителя. Нашите актуализации на състоянието на компонента очевидно се случват в областта JS. Това обикновено не е основен проблем, проблемът тук е, че тези актуализации се извършват на всеки кадър! Това означава, че за всеки един анимационен кадър, където искаме нещата да се чувстват най-течни, данните трябва да преминават през моста.

Това е затруднение в производителността, което чисто родните приложения нямат, което им улеснява много по-лесно достигането до светия граал от 60 FPS, особено на по-слаби устройства и особено в реални случаи, които са малко по-сложни от този пример.

Не прочетох ли нещо в документите за Директен манипулация?

Ако се интересувате от производителността, вероятно сте прочели документите за капак и неясно си спомняте тази статия за директното манипулиране на компонентите.

Звучи обещаващо, нека актуализираме нативния компонент директно и да подобрим производителността! Ще го опитаме, ето изпълнението:

Това реши ли нашите проблеми с производителността на моста? Всъщност не - тъй като все още се актуализираме от сферата на JS. Но това оптимизира нещо, което си струва да се разбере. В предишната им реализация на всеки кадър, който не изпращахме данни през моста, ние също рендерирахме нашия компонент. В този конкретен случай функцията за изобразяване почти нищо не прави, така че това не е проблем. Но какво ще стане, ако нашата функция за изобразяване е по-сложна и изчислително интензивна?

Традиционно, за да актуализираме компонент React, трябва да рендерираме. Ако нашата актуализация е много локализирана, като промяна на конкретен стил (x превод и непрозрачност), можем хирургически да го направим директно без пълното представяне и съгласуване. Това противоречи на "състоянието на ума" на React, така че е най-добре да не го правим често и да се ограничаваме до използване на случаи, при които конкретно свойство се променя много бързо (например по време на анимация).

Можем ли да се върнем към коригирането на проблема с моста?

Едно от най-красивите неща за React Native е, че можем да вземем всяко парче от нашата кодова база и да го преместим безпроблемно в родния си - дори само един компонент.

Разработчиците често грешат React Native като чиста JS среда - не е. Вярно е, че JS често дава най-доброто преживяване на разработчиците, но има случаи, когато native дава превъзходно потребителско изживяване. Призовавам ви, ако идвате от уеб фон - не се страхувайте от родното. Това е друг инструмент в колана, който обикновено отнема същото количество преливане на стака, за да упражнявате.

Тъй като сензорните събития произхождат от родния свят, какво би се случило, ако направим нашия x превод и актуализация на непрозрачността и в родния? Погледни:

Единствената част, която сме преместили на родния, е компонентът Swipeable контейнер. Това би ни гарантирало 60 FPS и изглежда, че кодът всъщност е по-къс. Забележете, че компонентите на съдържанието на нашата карта останаха в чисто JS. Ето как се използва родният ни клас в нашето JS оформление:

Бъдещето на React Native

Въпреки че е вярно, че можем да използваме нативен код избирателно, за да запушим нашите дупки в производителността, бъдещето на рамката е да подобрим и да се уверим, че трябва да правим това все по-малко и по-малко.

Възможно е да се проектират интелигентни JS интерфейси, които да сведат до минимум преминаванията през моста и да постигнат същите резултати. Ами ако в нашия пример, нашият JS код не трябваше да актуализира естествената област на всеки кадър? Ами ако бихме могли да посочим само веднъж, в началото на взаимодействието, кои свойства са заключени към кое родно събитие и да оставим някакъв нативен модул във вътрешния корем на React Native да зареди актуализациите за нас? Това би ни накарало да минем през моста само веднъж - в началото.

React Native се развива в тази посока и един от основните лакомства, които ни бяха дадени, е новата анимирана библиотека. Нека реализираме нашия пример за четвърти и последен път с Animated в чист JS:

Както можете да видите, Анимираната библиотека третира анимации и взаимодействия по много декларативен начин. Ако можем да декларираме как дадено взаимодействие се държи, тази декларация може да се сериализира и да се изпрати през моста. Това отваря възможност за общ роден модул да обработва взаимодействието за нас и да разтоварва рамката чрез актуализации на рамката.

За съжаление, настоящата (юни 2016 г.) реализация на Animated все още не зарежда всичко към родното. Това означава, че понастоящем нашата четвърта реализация все още страда от същото тясно препятствие. След като каза това, се постига напредък и аз съм убеден, че бъдещите версии ще ни позволят да преодолеем ограничението на моста от JS в много случаи.

Сравняване на четирите реализации

Да четеш за представянето не е същото като да го усетиш в реалния живот. Можете да играете с напълно работещи версии на четирите реализации в следващото репо, представено рамо до рамо за лесно сравнение:

Моля, стартирайте примера на действително устройство, тъй като симулаторът не дава автентични резултати. В допълнение, repo съдържа незадължителни флагове за симулиране на стресовите условия в приложението - като изблици на активност над моста и изчислително по-тежки функции на изобразяване. Интересно е да се проучи как се променя плавността на всяко изпълнение при тези условия.

Заключение и раздяла думи

Разработването на мобилни приложения в React Native е страхотно, но удобството понякога идва на цена. Възможно е обаче да се смекчи почти всеки проблем с представянето, а ключът е да разберем какво става под капака.

В Wix.com ние сме натрапчиви по отношение на UX и предоставянето на родно потребителско изживяване, което мобилните потребители очакваха. Ето нашето грубо ръководство за обсесивно изпълнение на React Native:

  1. Започнете с прилагането на всичко в JS за максимална производителност. Не преоптимизирайте прекалено рано.
  2. В области, които са предразположени към прекомерно използване на мостове, като анимации / взаимодействия - предпочитайте декларативните библиотеки като Анимирани. Много взаимодействия могат да бъдат изразени декларативно и дори, макар че в началото може да не е интуитивно (вижте нашата четвърта реализация), си струва да положите усилия.
  3. Изчакайте пълния продукт на истинско устройство, за да видите къде приложението ви изпада.
  4. Ако традиционните оптимизации на React се провалят, хирургично преместете проблемните части към родните. За тази цел поддържаме съотношение от около 10% местни разработчици в нашите инженерни екипи.
  5. Насърчавайте разработчиците на JS да се впускат в роден език. Това е мощен инструмент за използване на правилното място и не е извън обсега. Някои сложни взаимодействия не могат да бъдат изразени декларативно и разтоварени от библиотеки като Анимирани.