React Native - это весело. Если бы у вас был родной опыт работы с iOS или Android, вы бы осознали его возможности. Создать приложение очень просто и несложно. Тем не менее, одинаково легко испортить ваше приложение и сделать его медленным и запаздывающим, особенно если у вас есть большое приложение со сложной архитектурой, управлением состоянием и глубоко вложенными компонентами.

Даже в сложном приложении достижение 60 кадров в секунду с помощью React Native - это не ракетостроение. В этой статье я расскажу о типичных подводных камнях, которые приводят к медленным и нестабильным приложениям, и о том, как их избежать.

1. Помилуй конструктора

Я просматривал код своего коллеги, когда обнаружил что-то вроде этого:

class MyFavoriteComponent extends Component {

    constructor(props) {
        super(props)
        performSomeLongRunningOperation();
    }
    
}

Видите проблему здесь? Никогда не выполняйте действия, требующие времени для конструктора или componentWillMount. Предпочитаю переместить его на componentDidMount

2. Подсчитайте количество повторных рендеров

Ненужные повторные рендеры - причина №1, по которой большинство приложений React Native работают медленно. Используйте такие инструменты, как why-did-you-update, или добавьте простую точку останова или счетчик в render(), чтобы отслеживать ваши повторные рендеры и оптимизировать их. Кроме того, ниже мы обсудим причины, по которым можно избежать ненужного повторного рендеринга.

PS: повторный рендеринг компонента не обязательно означает, что весь компонент будет снова отрисован на нативной основе. Так как JS сначала вычисляет виртуальную модель DOM или дерево компонентов, которое затем проходит через механизм сравнения - и только окончательные различия перерисовываются в нативном коде. Однако отказ от ненужных вычислений повторного рендеринга может заметно повысить нашу производительность.

3. Будьте осторожны с функциональными реквизитами

Это причина №1 для ненужных обновлений компонентов. Все мы, должно быть, видели такие коды:

class MyComponent extends Component {
    
    render() {
        return (
            <SomeComplexComponent
                prop1="Hey, I'm prop1"
                prop2="Hey, I'm prop2"
                onPress={(id) => doSomething(id)}/>
        );
    }
    
}

Проблема: каждый раз, когда MyComponent повторно отображается его родительским элементом SomeComplexComponent, также будет повторно отображаться - даже если все реквизиты для SomeComplexComponent остается прежним, вызывая ненужные вычисления повторного рендеринга.

Причина: onPress содержит функцию стрелки. Итак, каждый раз, когда вызывается render() из MyComponent - создается новая ссылка на onPress - поскольку это функция стрелки. Таким образом, вынуждая SomeComplexComponent повторно визуализироваться без причины.

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

class MyComponent extends Component {
    
    render() {
        return (
            <SomeComplexComponent
                prop1="Hey, I'm prop1"
                prop2="Hey, I'm prop2"
                onPress={this.doSomething}/>
        );
    }

    doSomething = (id) => {
        this.setState({selectedId: id});
    }
}

4. По возможности отдавайте предпочтение чистым компонентам.

Чистые компоненты добавляют базовую оптимизацию к условиям повторного рендеринга компонентов. Таким образом, ваш компонент не будет обновлен, если ссылки на свойства и состояния останутся прежними. Это может отлично сработать во многих случаях использования, особенно если вы используете в своем приложении управление неизменяемым состоянием. Но также могут быть места, где вам может не понадобиться эта оптимизация.

Чтобы сделать ваш компонент чистым, просто выполните одно из следующих действий:

class MyComponent1 extends PureComponent {...}

@PureComponent
class MyComponent2 extends Component {...}

5. По возможности оптимизируйте shouldComponentUpdate ().

Рассмотрим случай ниже:

class ParentComponent extends Component {
    render() {
        return (
            <ChildComponent {...this.props.article} />
        )
    }
}

class ChildComponent extends Component {
    render() {
        return (
            <SomeComponent
                title={this.props.title}
                description={this.props.description}
                imageUrl={this.props.imageUrl}/>
        );
    }
}

В этом случае мы передаем некоторый список свойств от родительского компонента к дочернему. Однако если мы видим код - ChildComponent пользовательский интерфейс зависит только от title, description и imageUrl. Таким образом, мы не хотим, чтобы наш ChildComponent повторно отображался, если, скажем, изменится свойство с именем source. В этом случае мы очень уверены, что компонент необходимо повторно отрендерить только в том случае, если какая-либо опора из этих трех изменений изменилась. Для подобных случаев мы можем добавить дополнительную оптимизацию на shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState){
    return (nextProps.title !== this.props.title || nextProps.description !== this.props.description || nextProps.imageUrl !== this.props.imageUrl)
}

PS: Это очень простой пример с целью объяснения. Оптимизация shouldComponentUpdate обычно очень полезна для компонентов, входящих в список.

6. Используйте Reselect или Memoize с Redux

Давайте рассмотрим типичный mapStateToProps в сокращении:

const mapStateToProps = (state) => {
  return {
    data: computeData(state.someData, state.someCondition)
  }
}
const computeData = (someData, someCondition) {
    const data = {};
    data.x = process(someData.x, someCondition);
    data.y = process(someData.y, someCondition);
    return data;
}

Вы знаете, в чем проблема? При каждом проходе состояния redux мы создаем новую ссылку на данные. И если это свойство вводится в компонент, не имеющий shouldComponentUpdate оптимизаций для сравнения различных значений данных и проверки, требуется ли повторная визуализация, компонент будет повторно отображаться на каждом проходе redux.

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

7. Следите за анимацией.

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

  • На основе JS: при этом все вычисления кадра выполняются в потоке JS и последний кадр отправляется в собственный
  • Чисто родное: при этом вся анимация переносится в основной поток и требует минимального обмена данными или вообще его не требует.

Всегда лучше использовать чисто нативную анимацию. За подробностями я бы порекомендовал зайти сюда.

8. Используйте чистые родные навигаторы, если переходы между экранами по-прежнему резкие.

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

Как правило, для всех компонентов, которые визуализируются всякий раз, когда экран отображается первым, вы всегда должны использовать InteractionManager в componentDidMount() для любой задачи, требующей высокой производительности.

Кроме того, при выборе библиотеки навигатора - проверьте, происходит ли анимация перехода в нативном, а не в JS потоке. Это делает почти большинство хороших библиотек.

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

Реализация библиотек: react-native-navigation от Wix, native-navigation от Airbnb

9. Уважайте мост

Поскольку все непрерывно перетекает в мост RN и выходит из него - очень важно, чтобы мы помнили о том, как наши действия влияют на мост.

Рассмотрим пример использования ниже:

class MyComponent1 extends Component {

    componentDidMount() {
        shoot20ApiCallsParallelly();
    }

}

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

Чтобы этого избежать, рассмотрите возможность пакетирования вашего запроса и сохранения оптимального количества параллельных запросов. Кроме того, подобные проблемы возникают, если вы используете Websockets в своем приложении React Native - и перемещаете большое количество данных в / из вместе с взаимодействиями с пользовательским интерфейсом.

Чтобы узнать, что происходит в мосту, попробуйте использовать этот инструмент под названием snoopy в режиме разработки.

10. Другие взломы

Чтобы ускорить вашу работу в целом, вам следует учесть еще несколько вещей:

  • Используйте FlatList вместо ListView (что на данный момент довольно стандартно)
  • Удалите console.log в выпускаемых приложениях, особенно если вы используете библиотеки, такие как redux-logger, которые выгружают много данных на консоль. Плагин Babel transform-remove-console сделает это автоматически за вас.
  • Использование встроенных требований позволяет отложить загрузку дорогостоящих модулей в память. (Подумайте: lodash)
  • Потратьте некоторое время на профилирование своего приложения, чтобы глубже понять, что вызывает замедление работы вашего приложения.

Прощай!