Привет, Хабр! Меня зовут Вадим, я мобильный разработчик в СберМаркете. В этой статье расскажу, как провести профилирование (оно же измерение производительности или оценка перформанса) RN-приложений: как выявить источник проблем и решить их. В русскоязычных источниках не так много информации по данной теме. Я потратил немало времени, чтобы со всем разобраться, поэтому попытаюсь восполнить этот пробел и для вас :)
![](https://habrastorage.org/getpro/habr/upload_files/077/e8a/3b4/077e8a3b49356ae2eadad51ebd5b5250.jpg)
Зачем замерять производительность?
Это касается не только React Native, но и любых других технологий. Тест перформанса — это способ найти проблемные места, повысить производительность приложений и сделать их более удобными для пользователей.
Какие существуют метрики?
FPS. Частота кадров в секунду. Хочется, чтобы пользователи постоянно видели 60 кадров в секунду, к этому идеалу и будем стремиться.
Ресурсы процессора. За этой метрикой важно следить, потому что их чрезмерное использование приведёт к перегреву устройства и повышению энергопотребления. Девайс нагревается, значит быстрее садится батарейка, а мы не хотим, чтобы пользователь выходил из приложения с горячим и разряженным телефоном.
Энергопотребление. Эта метрика сильно завязана на потребление ресурсов процессора, но зависит и от других факторов. О них подробнее поговорим далее в статье.
Потребление памяти. Если за ним не следить, рано или поздно приложение может начать потреблять слишком много памяти, что замедлит его работу, а в худшем случае приведёт к крашу с ошибкой Out Of Memory.
Потребление сетевых ресурсов. Метрика, о которой следует знать, но воздействовать на неё мы можем ограниченно. По большей части, она зависит от того, как работает наш backend, а также от пропускной способности сети.
Что нужно сделать перед профилированием?
Очевидно, что приложение будет работать лучше на iPhone 15, чем на каком-нибудь стареньком Андроиде. Второй мы и выберем для профилирования:
во-первых, наше приложение будет не только у пользователей, владеющих сильными устройствами и так мы заранее позаботимся об их пользовательском опыте;
во-вторых, именно на слабых девайсах проще отловить проблемы с перформансом.
![](https://habrastorage.org/getpro/habr/upload_files/c78/490/e2f/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 монитор на примере:
Мы указываем количество времени в миллисекундах, которое будет длиться запись.
Нажимаем старт и производим действия, перфоманс которых хотим замерить, например, замер неоптимизированного списка с немемоизированными айтемами.
Стартуем запись и начинаем скроллить список. В прямом эфире видим, как ведёт себя JS FPS и UI FPS. На выходе получаем 75 баллов.
![](https://habrastorage.org/getpro/habr/upload_files/80a/8ea/3d7/80a8ea3d75f28ebd1558251685445b86.gif)
Видно, что средний JS FPS был 45. Допустим, что результат в 75 баллов нас не удовлетворяет, нужны улучшения. Мемоизируем лист айтемы и пробуем провести те же замеры.
![](https://habrastorage.org/getpro/habr/upload_files/136/ffb/f05/136ffbf05285cbe4b10e6da0df5e6a61.gif)
По графикам можно заметить, что приложение ведёт себя куда лучше. На выходе получаем 93 балла.
В итоге: Инструмент выполняет задачу верхнеуровневой оценки перформанса и позволяет понять, дали ли какой-то эффект оптимизации которые мы применили.
#2 React DevTools
Следующий инструмент – всем знакомый React DevTools. Его главное преимущество — в популярности. Он знаком подавляющему большинству React-разработчиков. Кроме того его можно использовать сразу : он встроен в Flipper, а также покрывает значительное количество кейсов, связанных с UI. Наконец, он максимально информативен — у него очень удобный UI.
React DevTools имеет две основные вкладки: Сomponents и Profiler. В первой можно видеть дерево элементов, а именно — как в нём представлены отдельные элементы.
![](https://habrastorage.org/getpro/habr/upload_files/96c/7a7/58a/96c7a758a336b9c171387ad19dc8a765.png)
Эту вкладку можно использовать для поиска проблемных мест. Однако для первичного анализа нас больше интересует вкладка профайлер.
Кстати, перед использованием этого инструмента в настройках рекомендую поставить галочку напротив пункта «Show why did component renders». Так вы сможете увидеть, почему какой-то конкретный компонент рендерился в процессе данного коммита.
Справа вверху виден список коммитов, которые были записаны в данной сессии. Коммит — это фаза, в которую React группирует все изменения представлений и отправляет для отрисовки в нативный поток. Между этими коммитами можно переключаться и смотреть в каком коммите произошли конкретные изменения.
Все элементы в React DevTools представлены в виде полосок, так называемых «баров», разной ширины и цвета:
Ширина говорит о том, сколько времени занял последний рендер компонента в процессе последнего коммита относительно других элементов.
-
Цвет бара говорит о том, сколько занял рендер в процессе выбранного коммита.
Серый говорит о том, что компонент вовсе не рендерился в процессе коммита.
Жёлтый — компонент довольно тяжелый в данном коммите по сравнению с остальными.
Зелёный — рендер был довольно лёгким.
Но моя любимая вкладка — Ranked: здесь компоненты отсортированы от наиболее тяжелого к наиболее лёгкому, поэтому можно очень просто найти, где возникают проблемы.
В примере ниже такой «проблемный» компонент — это Virtualized List, который тянется дольше всех и имеет желтый цвет во вкладке Flamegraph. В Ranked он также оказался в самом верху, что говорит о том, что он оказался самым тяжелым компонентом.
![](https://habrastorage.org/getpro/habr/upload_files/461/9a3/f71/4619a3f712e66cbb790dbdae1b5eb805.png)
Вернемся к примеру с немемоизированным списком. На графиках видно, какие компоненты рендерятся. Можно заметить неприятную полоску тёмно-зелёного цвета, в ней даже есть жёлтенькие квадратики. Это говорит о том, что очень много компонентов в процессе данного коммита были не очень лёгкими.
На самом деле, этот список небольшой — там не так много элементов, и все они довольно простые. В вашем приложении наверняка будут встречаться списки с более комплексными айтемами. Даже эти, казалось бы, не самые тяжелые элементы занимают, как можно видеть на вкладке Ranked, по 7-8 миллисекунд рендера.
Теперь мемоизируем элементы списка и посмотрим, что изменится.
![](https://habrastorage.org/getpro/habr/upload_files/f98/b7c/ebb/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, которая показывает нагрузку на процессор. Над ней будут появляться розовые точки, которые говорят о том, что произошло какое-то пользовательское событие.
Я начинаю скроллить список, и у нас появляются те самые события.
![](https://habrastorage.org/getpro/habr/upload_files/0b5/402/ada/0b5402ada76dc12b9cd86af711b94ca4.gif)
Нажимаем на кнопку и видим что нагрузка на процессор возросла, и значение пикселей при скролле теперь не меняется. Понимаем, что с большой долей вероятности проблема именно в js-потоке, так как пишем на RN и в первую очередь обращаем внимание именно на то что происходит там где лежит наша логика, а в нашем случае это JS код. Чтобы в этом убедиться, раскроем секцию CPU и посмотрим, какой поток у нас всё это время был занят работой. Видно, что поток mqt JS (это и есть наш JS поток) на протяжении 8.5 секунд был чем-то очень занят. Гипотеза подтвердилась.
![](https://habrastorage.org/getpro/habr/upload_files/be3/d3e/0a1/be3d3e0a1e7c1d53149c5df47a985820.png)
Казалось бы, дальше всё просто. Мы можем пойти в код и посмотреть, какая именно функция вызывается при обработке события клика на кнопку. В нашем случае, это veryHeavyFunction, которая считает факториал 200 тысяч раз. Но найти источник проблемы не всегда так легко :)
Как правило, обработчик напрямую не вызывает какую-то тяжелую функцию. Но он может вызывать какой-то сервис, который, в свою очередь, может вызывать другой сервис и так бесконечное число раз.
![](https://habrastorage.org/getpro/habr/upload_files/3a7/a09/4dc/3a7a094dc377a4c31a1d4467da0d3dee.png)
Чтобы нам провалиться в эту кроличью нору, обращаемся за помощью к Hermes Debugger.
Итак, идём в Hermes Debugger и записываем там эту же сессию. Нажимаем на кнопку «Call very Heavy function». Далее сортируем все вызванные функции по тотал тайму и видим, что, к примеру, 57% всего времени у нас было потрачено на выполнение функции «факториал». Это логично, потому что она реализована при помощи рекурсии и вызывала сама себя. Дело раскрыто! Дальше мы можем что-то сделать с этой функцией, чтобы облегчить нагрузку на процессор.
Энергопотребление
Как и обещал, чуть подробнее остановимся на этой метрике. Помимо нагрузки на процессор на энергопотребление могут влиять и другие факторы, которые не всегда лежат на поверхности. Среди них:
Wake lock — механизм, заставляющий процессор или экран работать в те моменты, когда пользователь, казалось бы, уже не пользуется приложением. Зачастую это касается приложений, которые показывают видео — ведь пользователь, запуская двухчасовое видео, вряд ли хочет, чтобы через минуту экран телефона заблокировался и видео остановилось.
Alarms — позволяют запускать какие-либо фоновые задачи вне контекста приложения.
Jobs — механизм, который позволяет выполнять какие-либо действия при изменении состояния (например, пропадание и появление сети).
Обращения к GPS сенсору для определения геопозиции.
![](https://habrastorage.org/getpro/habr/upload_files/22a/92a/036/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)](https://habrastorage.org/getpro/habr/upload_files/ea2/412/151/ea24121512e251d24da83a97789aa26f.png)
Вернёмся к нашему примеру, но теперь у нас есть кнопка Fill Memory каждое нажатие на которую создает большой JS объект и добавляет его в массив, за длинной которого мы постоянно наблюдаем, не позволяя тем самым сборщику мусора удалить его из памяти. Таким образом мы искусственно заполняем память JS объектами. На видео Objects created показывает кол-во созданных объектов. С каждым нажатием на кнопку график линейно растет.
![](https://habrastorage.org/getpro/habr/upload_files/c70/b10/265/c70b102651fb31b277cce7b24c916b58.gif)
Это ненормальная ситуация. У нас растёт секция others, то есть именно та секция, которая показывает потребление памяти JS-кодом.
Учитывая то, что код пишется на react native, мы первым делом подумаем, что проблема в JS. Видимо, создаём где-то слишком много тяжёлых объектов и не очищаем их. Чтобы убедиться в этом, заходим в Hermes Debugger.
![Hermes Debugger (Memory) Hermes Debugger (Memory)](https://habrastorage.org/getpro/habr/upload_files/012/6a5/64a/0126a564a811b19e722b03491fd1b4a4.png)
Во вкладке 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-менеджеров.