Sportsbet.com.au недавно перезапустил наше флагманское приложение для iOS. Созданный полностью на базе react-native и разделяющий примерно 60% кодовой базы с нашим сопровождающим адаптивным веб-сайтом, мы столкнулись с множеством проблем на этом пути, поскольку мы стремились сохранить свои позиции как лучшее приложение на высококонкурентном рынке.

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

Состав

В качестве библиотеки навигации мы используем react-navigation. Мы реализуем библиотеку react-native-screen, чтобы включить поддержку UIViewController для каждого экрана.

У нас есть приложение с 5 вкладками, и на каждой вкладке размещен навигатор стека, который позволяет пользователю переходить на более конкретные экраны на каждой вкладке. Состояние каждой вкладки сохраняется между переключениями вкладок - например, если вы нажмете на экран на вкладке A, переключитесь на вкладку B, а затем вернетесь на вкладку A - тогда вы увидите тот же контент, который просматривали до того, как сменили вкладки. Это стандартное поведение Apple HIG.

Мы также размещаем несколько других навигаторов стека в модальном стиле, которые используются для таких вещей, как экран входа в систему, экраны отключения и другие возможности в стиле «подмены» в приложении.

Навигатор

Когда вы используете реакцию-навигацию, вы обязаны создать NavigationContainer компонент, используя createAppContainer(...) . Затем вы визуализируете этот компонент в своем самом верхнем представлении в иерархии.

Наше приложение содержит (на момент написания) 93 различных маршрута и сопутствующие экраны.

Девяносто три!

Это тщательно контролируется с помощью switchNavigators и вложенных stackNavigators. У нас есть маршруты, которые доступны только после входа в систему. У нас есть тупики, в которые мы должны вести пользователя, если он использует наше приложение в определенных географических регионах. В некоторых модальных окнах нельзя использовать жесты смахивания. Мы переопределяем заголовок stackNavigator, чтобы он выглядел по-другому как минимум в трех условиях. Просто тонна бизнес-логики и правил помещена в иерархию навигации нашего приложения.

Цена

Поддержка всех этих маршрутов означала импорт компонента, который навигатор отображает для каждого маршрута. Это означает, что мы импортировали (более) 93 компонентов из других модулей в наш файл global_navigator.tsx. Это много импорта и синтаксического анализа, но это казалось необходимым, поскольку нет возможности разделить или динамически добавлять маршруты к вашему NavigationContainer (результат createAppContainer(...)) после того, как вы его создали. Это большая старая императивная операция - «создать все эти маршруты и использовать эти компоненты в качестве экранов».

У нас было что-то вроде этого:

// global_navigator.tsx
...
import { RacecardRaceReplayScreen } from "../../../racecard/components_ios/racecard_race_replay_screen"
import { RacecardScreen } from "../../../racecard/components_ios/racecard_screen"
import { RacingLiveStreamingPlayer } from "../../../racecard/components_ios/racecard_streaming_player"
// do this 90 more times 
... 
// (inside the definition of a stack navigator)
...
},
"/FullScreenRacingReplayVideoStreaming": {
  screen: (props: NavigationScreenProps) => (
    <RacecardRaceReplayScreen navigation={props.navigation} />
  ),
  navigationOptions: {
    gesturesEnabled: false
  }
},
...

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

Решение

Пакеты оперативной памяти и встроенные требования

Мы реорганизовали наш навигатор, чтобы использовать React.lazy и Suspense как средство для импорта кода из других модулей только тогда, когда они необходимы.

...
let RacecardRaceReplayScreen: React.LazyExoticComponent<Any>
...
// inside the definition of a stack navigator
"/FullScreenRacingReplayVideoStreaming": {
  screen: (props: NavigationScreenProps) => {
    if (!RacecardRaceReplayScreen) {
      RacecardRaceReplayScreen = React.lazy(() => import("../../../racecard/components_ios/racecard_race_replay_screen").then(imported => {
        return { default: imported.RacecardRaceReplayScreen }
      })
    )
  }
  return (
      <Suspense fallback={null}>
        <RacecardRaceReplayScreen navigation={props.navigation} />
      </Suspense>
  )
  },
  navigationOptions: {
    gesturesEnabled: false
  }
},
...

Вместо того, чтобы импортировать все компоненты из модуля global_navigator при запуске, они импортируются только тогда, когда stackNavigator просят отобразить экран для определенного маршрута. После импорта компонент становится доступным для повторного использования в более позднее время с помощью переменной let, которая существует в модуле.

React.lazy возвращает объект со специальным свойством type Symbol(react.lazy), этот объект может быть превращен в элемент с помощью React, и React сохраняет примечание об этом type для последующего изучения. Когда код рендеринга React пытается отобразить этот элемент, он видит, что это ленивый компонент и ему необходимо вызвать другую функцию, чтобы получить желаемый контент для рендеринга - это функция, которую вы указали в качестве аргумента для React.lazy () вызов). Вы предоставляете функцию типа Promise (то, что затем возможно), которая выполняется кодом рендеринга React, и он будет искать возвращаемый объект вашего обещания, который либо является экспортом по умолчанию для этого модуля, либо, если вам нравится us и используйте именованный экспорт, вы захотите вернуть объект, который имитирует экспорт по умолчанию ala

return { default: imported.RacecardRaceReplayScreen }

НАКОНЕЦ! В React есть ваш компонент, ваш экран - то, что вы хотите визуализировать для определенного маршрута. Затем React вставляет ваш компонент в дерево, и ваши пользователи видят ваш экран.

Я упростил то, что происходит под капотом - если вам интересно, я рекомендую пройти через код React, чтобы увидеть, что происходит.

Выгоды

Устройство: iPhone 6
До изменения: в среднем 7,2 секунды
После изменения: в среднем 5,6 секунды

После того, как мы приняли вышеуказанное изменение, мы заметили сокращение общего времени запуска нашего приложения. Чем старше устройство, тем больше экономия.

Наше приложение поддерживает iOS 9 и выше, и у нас есть нетривиальное количество клиентов на таких старых устройствах, как iPhone 6 (чуть менее 6% наших активных пользователей). Именно эти пользователи видят наибольшую выгоду от этого изменения.

У нас есть программное обеспечение для автоматизации, которое записывает пользовательский опыт для запуска приложений. Приложение запускается, и измеряется время, которое проходит от значка до начала увеличения до полного отображения домашнего экрана (больше ничего не нужно перемещать). Этот тест включает в себя набор HTTPS-вызовов для получения содержимого домашней страницы внутри приложения и дает представление о том, «что будут испытывать наши клиенты» при запуске приложения. Мы запускаем этот тест 5 раз и берем среднее значение. Приложение прерывается между каждым запуском, так что по сути это теплые запуски - с точки зрения разработки iOS.

Эффективность запуска приложений - это очень обширная тема, и это не должно быть единственной мерой, которую вы должны пробовать, чтобы уменьшить время ожидания (или ожидания) ваших пользователей до тех пор, пока ваше приложение не станет интерактивным. Есть такие вещи, как отложенная загрузка панелей вкладок, кеширование HTTP-ответов и представление элементов-заполнителей, которые также могут дать вашим пользователям ощутимое сокращение времени запуска.

Если вы используете FlatList - воспользуйтесь опорой initialNumToRender, чтобы вы загружали только элементы «выше сгиба» (видимые на экране).

Если вы используете Android, есть классный пост от Mattermost, который стоит прочитать. Вам, вероятно, также стоит попробовать JS-движок Hermes!