Анализ производительности React Native приложений: как выявить проблемы и улучшить перформанс

Привет, Хабр! Меня зовут Вадим, я мобильный разработчик в СберМаркете. В этой статье расскажу, как провести профилирование (оно же измерение производительности или оценка перформанса) RN-приложений: как выявить источник проблем и решить их. В русскоязычных источниках не так много информации по данной теме. Я потратил немало времени, чтобы со всем разобраться, поэтому попытаюсь восполнить этот пробел и для вас :)

077e8a3b49356ae2eadad51ebd5b5250.jpg

Зачем замерять производительность?

Это касается не только React Native, но и любых других технологий. Тест перформанса — это способ найти проблемные места, повысить производительность приложений и сделать их более удобными для пользователей.

Какие  существуют метрики?

  • FPS. Частота кадров в секунду. Хочется, чтобы пользователи постоянно видели 60 кадров в секунду, к этому идеалу и будем стремиться.

  • Ресурсы процессора. За этой метрикой важно следить, потому что их чрезмерное использование приведёт к перегреву устройства и повышению энергопотребления. Девайс нагревается, значит быстрее садится батарейка, а мы не хотим, чтобы пользователь выходил из приложения с горячим и разряженным телефоном.

  • Энергопотребление. Эта метрика сильно завязана на потребление ресурсов процессора, но зависит и от других факторов. О них подробнее поговорим далее в статье. 

  • Потребление памяти. Если за ним не следить, рано или поздно приложение может начать потреблять слишком много памяти, что замедлит его работу, а в худшем случае приведёт к крашу с ошибкой Out Of Memory. 

  • Потребление сетевых ресурсов. Метрика, о которой следует знать, но воздействовать на неё мы можем ограниченно. По большей части, она зависит от того, как работает наш backend, а также от пропускной способности сети.

Что нужно сделать перед профилированием?

Очевидно, что приложение будет работать лучше на iPhone 15, чем на каком-нибудь стареньком Андроиде. Второй мы и выберем для профилирования:

  • во-первых, наше приложение будет не только у пользователей, владеющих сильными устройствами и так мы заранее позаботимся об их пользовательском опыте;

  • во-вторых, именно на слабых девайсах проще отловить проблемы с перформансом.

c78490e2fb6f80e32c65ed82d1eafc72.png

Я рекомендую перед профилированием отключить Dev Mode. Так мы снимаем с процессора ответственность за то, чтобы он выполнял работу по проверке некоторых типов (например, prop types). Ещё мы избавляем приложение от необходимости подготавливать для нас удобные логи и ворнинги. Это приближает наш опыт к опыту реального юзера.

О чем важно помнить в процессе профилирования?

1.Все тесты нужно проводить несколько раз — так мы получим более или менее объективный результат. Например, если просто скроллить список с обычной скоростью, можно сперва не заметить проблем, но если проскроллить этот же список в два раза быстрее, то вероятность заметить глитчи, лаги и прочие проблемы увеличивается.

2. Усреднять результаты замеров. Например мы проводим тест скролла списка и делаем это 5 раз чтобы получить объективный результат и получаем значения ФПС — 50, 56, 52, 50, 58. Но работать с несколькими значениями одновременно крайне неудобно, поэтому просто берем среднее арифметическое и работаем со значением FPS = 54.

3. Между замерами необходимо сохранять одно состояние девайса, чтобы получать максимально детерминированный результат. Если мы будем замерять производительность, когда у нас в бэкграунде нет никаких процессов и когда запущено ещё N процессов — с большой долей вероятности, мы получим разные результаты.

4. По возможности, автоматизировать процесс профилирования. Замеры в некоторых случаях рекомендуется проводить от 100 до бесконечности раз, и чтобы 100 раз не запускать инструмент в ручном режиме, это дело можно и нужно автоматизировать. Один из инструментов, о котором поговорим дальше, можно автоматизировать при помощи adb.

#1 Perf Monitor

Перейдём к инструментам. Первый из них — RN perf Monitor, встроенный во Flipper. 

Какие у него преимущества? RN perf Monitor — это своеобразный LightHouse для React Native приложений и он довольно легко подключается. Здесь мы видим три основных метрики:  

  • средний FPS JS потока;  

  • средний FPS UI потока ;

  • так называемый JS threadlock, то есть время, когда JS поток был «заблокирован» — выполнял какую-то работу и не мог занимать больше ресурса. 

Предположу, что это самый быстрый способ верхнеуровнево оценить перфоманс. Результат будет зависеть от используемого девайса. 

Целевые значения. К чему нужно стремиться?

JS FPS

всегда > 0

UI FPS

максимально близок к 60

JS Threadlock

стремился к нулю

Итоговая оценка перфоманса

100 баллов*

*Авторы плагина пишут в документации, что 100 баллов можно получить, только если запустите чистое приложение. Как только вы начнёте что-то добавлять или исправлять — 100 баллов вы никогда не увидите. Падает оценка банально потому что в приложении появляется какая-то логика, которая нагружает процессор. Каждый для себя решает, какой показатель «достаточный», в первую очередь это подходит для отслеживания прогресса — если было 60, а стало 65, значит стало лучше, и наоборот :)

Рассмотрим как работает RN perf монитор на примере:  

  1. Мы указываем количество времени в миллисекундах, которое будет длиться запись. 

  2. Нажимаем старт и производим действия, перфоманс которых хотим замерить, например, замер неоптимизированного списка с немемоизированными айтемами.

Стартуем запись и начинаем скроллить список. В прямом эфире видим, как ведёт себя JS FPS и UI FPS. На выходе получаем 75 баллов. 

80a8ea3d75f28ebd1558251685445b86.gif

Видно, что средний JS FPS был 45. Допустим, что результат в 75 баллов нас не удовлетворяет, нужны улучшения. Мемоизируем лист айтемы и пробуем провести те же замеры. 

136ffbf05285cbe4b10e6da0df5e6a61.gif

По графикам можно заметить, что приложение ведёт себя куда лучше. На выходе получаем 93 балла.

В итоге: Инструмент выполняет задачу верхнеуровневой оценки перформанса и позволяет понять, дали ли какой-то эффект оптимизации которые мы применили.

#2 React DevTools

Следующий инструмент — всем знакомый React DevTools. Его главное преимущество — в популярности. Он знаком подавляющему большинству React-разработчиков. Кроме того его можно использовать сразу : он встроен в Flipper, а также покрывает значительное количество кейсов, связанных с UI. Наконец, он максимально информативен — у него очень удобный UI. 

React DevTools имеет две основные вкладки: Сomponents и Profiler. В первой можно видеть дерево элементов, а именно — как в нём представлены отдельные элементы. 

96c7a758a336b9c171387ad19dc8a765.png

Эту вкладку можно использовать для поиска проблемных мест. Однако для первичного анализа нас больше интересует вкладка профайлер.

Кстати, перед использованием этого инструмента в настройках рекомендую поставить галочку напротив пункта «Show why did component renders». Так вы сможете увидеть, почему какой-то конкретный компонент рендерился в процессе данного коммита.

Справа вверху виден список коммитов, которые были записаны в данной сессии. Коммит — это фаза, в которую React группирует все изменения представлений и отправляет для отрисовки в нативный поток. Между этими коммитами можно переключаться и смотреть в каком коммите произошли конкретные изменения.

Все элементы в React DevTools представлены в виде полосок, так называемых «баров», разной ширины и цвета:

  • Ширина говорит о том, сколько времени занял последний рендер компонента в процессе последнего коммита относительно других элементов. 

  • Цвет бара говорит о том, сколько занял рендер в процессе выбранного коммита.

    • Серый говорит о том, что компонент вовсе не рендерился в процессе коммита.

    • Жёлтый — компонент довольно тяжелый в данном коммите по сравнению с остальными.

    • Зелёный — рендер был довольно лёгким.  

Но моя любимая вкладка —  Ranked: здесь компоненты отсортированы от наиболее тяжелого к наиболее лёгкому, поэтому можно очень просто найти, где возникают проблемы. 

В примере ниже такой «проблемный» компонент — это Virtualized List, который тянется дольше всех и имеет желтый цвет во вкладке Flamegraph. В Ranked он также оказался в самом верху, что говорит о том, что он оказался самым тяжелым компонентом.

4619a3f712e66cbb790dbdae1b5eb805.png

Вернемся к примеру с немемоизированным списком. На графиках видно, какие компоненты рендерятся. Можно заметить неприятную полоску тёмно-зелёного цвета, в ней даже есть жёлтенькие квадратики. Это говорит о том, что очень много компонентов в процессе данного коммита были не очень лёгкими. 

На самом деле, этот список небольшой — там не так много элементов, и все они довольно простые. В вашем приложении наверняка будут встречаться списки с более комплексными айтемами. Даже эти, казалось бы, не самые тяжелые элементы занимают, как можно видеть на вкладке Ranked, по 7–8 миллисекунд рендера. 

Теперь мемоизируем элементы списка и посмотрим, что изменится.

f98b7cebb6534714b7d905bafd3f41c6.png

Теперь рендерятся только ячейки списка, а сам контент ячеек отрисовывается только один раз. Во вкладке Ranked видно, что все компоненты у нас зелёненькие. На самом деле они не стали легче — просто теперь тяжелый контент ячеек вовсе не рендерится повторно и поэтому не отображается в этой вкладке, так как в ней отображаются только те компоненты, которые рендерились в процессе выбранного коммита. 

В итоге: Инструмент отлично подходит для поиска проблемных компонентов. 

#3 Android Studio Profiler + Hermes Debugger

Следующая пара инструментов — Android Studio Profiler и Hermes Debugger. Я рассматриваю их в связке:

  • Android Studio Profiler имеет довольно широкий функционал. Его подключение не требует каких-то особых манипуляций, поскольку он уже встроен в Android Studio. Однако он даёт мало информации о том, что происходит в js-потоке. 

  • Чтобы углубиться в это, у нас есть Hermes Debugger, который отлично дополняет Android Studio в этом плане.

Вот пример замеров того, как ведёт себя процессор в Android Studio Profiler. Справа открыт эмулятор с демо-приложением. Видно скролл и значение пикселей, проскроленных от начала списка. Ещё есть кнопка Call Heavy Function, которая вызывает какую-то тяжёлую функцию. Есть секция CPU, которая показывает нагрузку на процессор. Над ней будут появляться розовые точки, которые говорят о том, что произошло какое-то пользовательское событие. 

Я начинаю скроллить список, и у нас появляются те самые события.

0b5402ada76dc12b9cd86af711b94ca4.gif

Нажимаем на кнопку и видим что нагрузка на процессор возросла, и значение пикселей при скролле теперь не меняется. Понимаем, что с большой долей вероятности проблема именно в js-потоке, так как пишем на RN и в первую очередь обращаем внимание именно на то что происходит там где лежит наша логика, а в нашем случае это JS код. Чтобы в этом убедиться, раскроем секцию CPU и посмотрим, какой поток у нас всё это время был занят работой. Видно, что поток mqt JS (это и есть наш JS поток) на протяжении 8.5 секунд был чем-то очень занят. Гипотеза подтвердилась.

be3d3e0a1e7c1d53149c5df47a985820.png

Казалось бы, дальше всё просто. Мы можем пойти в код и посмотреть, какая именно функция вызывается при обработке события клика на кнопку. В нашем случае, это veryHeavyFunction, которая считает факториал 200 тысяч раз. Но найти источник проблемы не всегда так легко :) 

Как правило, обработчик напрямую не вызывает какую-то тяжелую функцию. Но он может вызывать какой-то сервис, который, в свою очередь, может вызывать другой сервис и так бесконечное число раз.

3a7a094dc377a4c31a1d4467da0d3dee.png

Чтобы нам провалиться в эту кроличью нору, обращаемся за помощью к Hermes Debugger. 

Итак, идём в Hermes Debugger и записываем там эту же сессию. Нажимаем на кнопку «Call very Heavy function». Далее сортируем все вызванные функции по тотал тайму и видим, что, к примеру, 57% всего времени у нас было потрачено на выполнение функции «факториал». Это логично, потому что она реализована при помощи рекурсии и вызывала сама себя. Дело раскрыто! Дальше мы можем что-то сделать с этой функцией, чтобы облегчить нагрузку на процессор.

Энергопотребление

Как и обещал, чуть подробнее остановимся на этой метрике. Помимо нагрузки на процессор на энергопотребление могут влиять и другие факторы, которые не всегда лежат на поверхности. Среди них:  

  • Wake lock — механизм, заставляющий процессор или экран работать в те моменты, когда пользователь, казалось бы, уже не пользуется приложением. Зачастую это касается приложений, которые показывают видео — ведь пользователь, запуская двухчасовое видео, вряд ли хочет, чтобы через минуту экран телефона заблокировался и видео остановилось. 

  • Alarms — позволяют запускать какие-либо фоновые задачи вне контекста приложения.

  • Jobs — механизм, который позволяет выполнять какие-либо действия при изменении состояния (например, пропадание и появление сети).

  • Обращения к GPS сенсору для определения геопозиции.

22a92a03624a7f223b6ed3a7a35b0b02.png

Вот так выглядит раскрытая секция энергопотребления в профайлере Android Studio. Наведя на любой участок, можно увидеть, из чего складывается энергопотребление в конкретный момент времени. 

Например, я выбрал участок и вижу: в данный момент энергопотребление находится на среднем уровне и можно увидеть из чего оно складывается:

  • средняя нагрузка на процессор (главная причина данного уровня энергопотребления);

  • незначительная нагрузка на сеть и отсутствие нагрузки на локацию.

  • Wake Locks: 1 — говорит о том что какой то один Wake Lock активен в данный момент (обозначается красной полоской снизу) и оказывает непосредственное влияние на энергопотребление. 

  • Alarms & Jobs: 0 и Location: 0 — говорит о том что никаких джобов и обращений к GPS сенсору соответственно в данный момент нет.

Анализ памяти

Напоследок, поговорим про анализ памяти. Вот так выглядит раскрытая секция памяти в профайлере Android Studio. Сверху видно, из чего складывается потребление памяти, например, нативный код, стек и тд. Но нас  интересует секция others, потому что именно там находится наш JavaScript. 

Android Studio Profiler (Memory)

Android Studio Profiler (Memory)

Вернёмся к нашему примеру, но теперь у нас есть кнопка Fill Memory каждое нажатие на которую создает большой JS объект и добавляет его в массив, за длинной которого мы постоянно наблюдаем, не позволяя тем самым сборщику мусора удалить его из памяти. Таким образом мы искусственно заполняем память JS объектами. На видео Objects created показывает кол-во созданных объектов. С каждым нажатием на кнопку график линейно растет.

c70b102651fb31b277cce7b24c916b58.gif

Это ненормальная ситуация. У нас растёт секция others, то есть именно та секция, которая показывает потребление памяти JS-кодом.

Учитывая то, что код пишется на react native, мы первым делом подумаем, что проблема в JS. Видимо, создаём где-то слишком много тяжёлых объектов и не очищаем их. Чтобы убедиться в этом, заходим в Hermes Debugger. 

Hermes Debugger (Memory)

Hermes Debugger (Memory)

Во вкладке Memory можно записать ту же самую сессию, которую мы проводили в профайлере Android Studio. По завершению записи, мы видим результат во вкладке «статистика». Так можно в процессе записанной сессии посмотреть, из чего вообще складывается потребляемая память. Видно, что довольно большой объём памяти занимают массивы. 

Самый простой способ — отсортировать объекты по Ritained Size или Shallow Size:

  • Shallow Size — это размер самого объекта. Он не учитывает размеры объектов, на которые объект ссылается. 

  • Ritained Size — размер самого объекта и всех объектов, на которые он ссылается. 

Видно, что наибольший объём памяти у нас занимает JS array. Раскрыв его, становится понятно, что он состоит из объектов, у которых есть свойства. По ним уже можно будет определить, что является источником проблемы. Если вы видите, например, объекст с полем Order Number, то понятно, что много памяти занимает объект какого-то заказа, и с этим уже можно работать. Таким образом, мы нашли объект, который забил всю память и не удалялся из памяти на протяжении записанной сессии.

Выводы

На самом деле, всегда есть что улучшать, и производительность — не исключение. Не останавливайтесь на рассмотренных мной инструментах — поищите новые, используйте их в связке. Я для себя открыл связку Android Studio Profiler и Hermes Debugger, которые, на мой взгляд, отлично друг друга дополняют. 

Можно посмотреть доклад Александра Моура — это один из создателей RN perf monitor. Это довольно полезный инструмент, позволяющий оценить перформанс на верхнем уровне и сразу посмотреть, улучшилось ли что-то у вас после каких-то манипуляций или нет.

Также посмотрите в сторону XCode instruments. Это аналог Android Studio профайлера, но для iOS. В этой статье мы рассмотрели только Android, потому что лучше выбирать для профилирования слабый Android девайс. Однако могут возникать специфичные для iOS баги — тут и может пригодиться XCode instrument.

Спасибо за внимание. Буду рад ответить на ваши вопросы в комментариях!

Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.

© Habrahabr.ru