Перейти к содержанию

Обзор производительности

Убедительной причиной для использования React Native вместо инструментов на базе WebView является достижение 60 кадров в секунду и "родной" внешний вид и ощущение от ваших приложений. Там, где это возможно, мы хотели бы, чтобы React Native делал все правильно и помогал вам сосредоточиться на вашем приложении, а не на оптимизации производительности, но есть области, где мы еще не дошли до конца, и другие, где React Native (подобно написанию нативного кода напрямую) не может определить лучший способ оптимизации для вас, поэтому потребуется ручное вмешательство. Мы стараемся сделать все возможное, чтобы обеспечить плавную работу пользовательского интерфейса по умолчанию, но иногда это невозможно.

Это руководство призвано научить вас некоторым основам, которые помогут вам устранить проблемы с производительностью, а также обсудить общие источники проблем и предлагаемые решения.

Что нужно знать о кадрах

Поколение ваших бабушек и дедушек называло фильмы "движущимися картинками" не просто так: реалистичное движение в видео — это иллюзия, создаваемая быстро меняющимися статичными изображениями с постоянной скоростью. Мы называем каждое из этих изображений кадром. Количество кадров, отображаемых каждую секунду, напрямую влияет на плавность и реалистичность видео (или пользовательского интерфейса). Устройства iOS отображают 60 кадров в секунду, что дает вам и системе пользовательского интерфейса около 16,67 мс для выполнения всей работы, необходимой для создания статичного изображения (кадра), которое пользователь увидит на экране в этот промежуток времени. Если вы не сможете выполнить работу, необходимую для создания этого кадра в течение отведенных 16,67 мс, то вы "провалите кадр", и пользовательский интерфейс окажется неотзывчивым.

Теперь, чтобы немного запутать дело, откройте Dev Menu в вашем приложении и переключите Show Perf Monitor. Вы заметите, что есть две разные частоты кадров.

Что нужно знать о кадрах

Частота кадров JS (поток JavaScript)

В большинстве приложений React Native ваша бизнес-логика будет выполняться в потоке JavaScript. Именно здесь живет ваше приложение React, выполняются вызовы API, обрабатываются события касания и т.д.. Обновления представлений, поддерживаемых нативной средой, группируются и передаются нативную сторону в конце каждой итерации цикла событий, до истечения срока выполнения кадра (если все идет хорошо). Если поток JavaScript не реагирует на фрейм, он будет считаться пропущенным. Например, если вы вызовете this.setState на корневом компоненте сложного приложения и это приведет к перерисовке вычислительно дорогих поддеревьев компонентов, то вполне возможно, что это займет 200 мс и приведет к тому, что 12 кадров будут потеряны. Любая анимация, управляемая JavaScript, будет выглядеть застывшей в течение этого времени. Если что-то займет больше 100 мс, пользователь почувствует это.

Это часто происходит во время переходов Навигатора: когда вы прокладываете новый маршрут, поток JavaScript должен отрисовать все компоненты, необходимые для сцены, чтобы передать соответствующие команды нативной стороне для создания вспомогательных представлений. Обычно работа, выполняемая здесь, занимает несколько кадров и вызывает jank, поскольку переход контролируется потоком JavaScript. Иногда компоненты выполняют дополнительную работу по componentDidMount, что может привести к секундной заминке при переходе.

Другой пример — реакция на прикосновения: если вы выполняете работу в нескольких кадрах в потоке JavaScript, вы можете заметить задержку в реакции, например, на TouchableOpacity. Это происходит потому, что поток JavaScript занят и не может обработать необработанные события касания, передаваемые из главного потока. В результате TouchableOpacity не может отреагировать на события касания и дать команду нативному представлению изменить свою непрозрачность.

Частота кадров пользовательского интерфейса (основной поток)

Многие заметили, что производительность NavigatorIOS из коробки лучше, чем Navigator. Причина этого в том, что анимация переходов выполняется полностью в основном потоке, и поэтому она не прерывается падениями кадров в потоке JavaScript.

Аналогично, вы можете спокойно прокручивать вверх и вниз ScrollView, когда поток JavaScript заблокирован, потому что ScrollView живет в основном потоке. События прокрутки отправляются в поток JS, но их получение не обязательно для того, чтобы произошла прокрутка.

Общие источники проблем с производительностью

Запуск в режиме разработки (dev=true)

Производительность потока JavaScript сильно падает при работе в режиме разработки. Это неизбежно: во время выполнения нужно проделать гораздо больше работы, чтобы обеспечить вас хорошими предупреждениями и сообщениями об ошибках, например, проверить propTypes и различные другие утверждения. Всегда проверяйте производительность в release builds.

Использование утверждений console.log.

При запуске приложения в комплекте эти утверждения могут вызвать большое узкое место в потоке JavaScript. Сюда относятся вызовы отладочных библиотек, таких как redux-logger, поэтому убедитесь, что они удалены перед обвязкой. Вы также можете использовать этот babel plugin, который удаляет все вызовы console.*. Вам нужно сначала установить его с помощью npm i babel-plugin-transform-remove-console --save-dev, а затем отредактировать файл .babelrc в каталоге вашего проекта следующим образом:

1
2
3
4
5
6
7
{
    "env": {
        "production": {
            "plugins": ["transform-remove-console"]
        }
    }
}

Это автоматически удалит все вызовы console.* в релизных (продакшн) версиях вашего проекта.

Рекомендуется использовать плагин, даже если в вашем проекте нет вызовов console.*. Сторонняя библиотека также может вызывать их.

ListView слишком медленно отрисовывается или плохо работает прокрутка для больших списков.

Вместо этого используйте новый компонент FlatList или SectionList. Помимо упрощения API, новые компоненты списков также имеют значительные улучшения производительности, главным из которых является почти постоянное использование памяти для любого количества строк.

Если ваш FlatList рендерится медленно, убедитесь, что вы реализовали getItemLayout для оптимизации скорости рендеринга, пропуская измерения рендеримых элементов.

JS FPS падает при повторном рендеринге представления, которое почти не меняется

Если вы используете ListView, вы должны предоставить функцию rowHasChanged, которая может сократить много работы, быстро определяя, нужно ли повторно рендерить строку. Если вы используете неизменяемые структуры данных, это должна быть только проверка равенства ссылок.

Аналогично, вы можете реализовать shouldComponentUpdate и указать точные условия, при которых вы бы хотели, чтобы компонент был обновлен. Если вы пишете чистые компоненты (где возвращаемое значение функции рендеринга полностью зависит от пропсов и состояния), вы можете использовать PureComponent, чтобы сделать это за вас. Опять же, неизменяемые структуры данных полезны для обеспечения быстродействия — если вам нужно провести глубокое сравнение большого списка объектов, может оказаться, что перерендеринг всего компонента будет быстрее, и это, конечно, потребует меньше кода.

Падение FPS потока JS из-за одновременного выполнения большого количества работы в потоке JavaScript

"Медленные переходы навигатора" — наиболее распространенное проявление этого, но есть и другие случаи, когда это может произойти. Использование InteractionManager может быть хорошим подходом, но если стоимость пользовательского опыта слишком высока для задержки работы во время анимации, то вам стоит рассмотреть LayoutAnimation.

В настоящее время Animated API вычисляет каждый ключевой кадр по требованию в потоке JavaScript, если вы не установите useNativeDriver: true, в то время как LayoutAnimation использует Core Animation и не подвержен влиянию падений кадров в потоке JS и основном потоке.

В одном из случаев я использовал это для анимации в модале (скольжение сверху вниз и затухание в полупрозрачном наложении) во время инициализации и, возможно, получения ответов на несколько сетевых запросов, рендеринга содержимого модала и обновления представления, из которого был открыт модал. Дополнительную информацию об использовании LayoutAnimation см. в руководстве Animations.

Предостережения:

  • LayoutAnimation работает только для анимаций типа "выстрелил и забыл" ("статические" анимации) — если она должна быть прерываемой, вам нужно использовать Animated.

Перемещение представления на экране (прокрутка, перевод, вращение) снижает FPS потока UI.

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

Будьте осторожны, не переусердствуйте, иначе потребление памяти может возрасти. При использовании этих пропсов оцените производительность и использование памяти. Если вы больше не планируете перемещать вид, отключите это свойство.

Анимация размера изображения снижает FPS потока пользовательского интерфейса

На iOS каждый раз, когда вы изменяете ширину или высоту компонента Image, он повторно уменьшается и масштабируется по сравнению с исходным изображением. Это может быть очень дорого, особенно для больших изображений. Вместо этого используйте свойство стиля transform: [{scale}] для изменения размера. Примером того, как это можно сделать, является касание изображения и увеличение его на весь экран.

Мое представление TouchableX не очень отзывчивое

Иногда, если мы выполняем действие в том же кадре, в котором мы регулируем непрозрачность или подсветку компонента, реагирующего на прикосновение, мы не увидим этого эффекта до тех пор, пока функция onPress не вернется. Если onPress выполняет setState, что приводит к большому объему работы и потере нескольких кадров, это может произойти. Решение этой проблемы — обернуть любое действие внутри обработчика onPress в requestAnimationFrame:

1
2
3
4
5
handleOnPress() {
  requestAnimationFrame(() => {
    this.doExpensiveAction();
  });
}

Медленные переходы навигатора

Как упоминалось выше, анимация навигатора управляется потоком JavaScript. Представьте себе переход сцены "push from right": каждый кадр новая сцена перемещается справа налево, начиная за пределами экрана (допустим, при x-смещении 320) и в конечном итоге завершаясь, когда сцена оказывается на x-смещении 0. Каждый кадр во время этого перехода поток JavaScript должен посылать новое x-смещение главному потоку. Если поток JavaScript заблокирован, он не может этого сделать, поэтому обновление на этом кадре не происходит, и анимация замирает.

Одним из решений этой проблемы является возможность перегрузить анимацию на основе JavaScript в основной поток. Если бы мы хотели сделать то же самое, что и в приведенном выше примере, мы могли бы вычислить список всех x-смещений для новой сцены, когда мы начинаем переход, и отправить их в основной поток для выполнения оптимизированным способом. Теперь, когда поток JavaScript освобожден от этой ответственности, нет ничего страшного, если он пропустит несколько кадров при рендеринге сцены — вы, вероятно, даже не заметите этого, потому что будете слишком отвлечены красивым переходом.

Решение этой проблемы — одна из основных целей новой библиотеки React Navigation. Представления в React Navigation используют нативные компоненты и библиотеку Animated для создания анимации с частотой 60 FPS, которая выполняется в нативном потоке.

Комментарии