Skip to content

Latest commit

 

History

History
146 lines (96 loc) · 27.6 KB

2-react-native-at-airbnb-the-technology.md

File metadata and controls

146 lines (96 loc) · 27.6 KB

この記事はAirbnb社におけるReact Native体験と 次の時代のモバイルアプリケーション開発について記したブログシリーズの第二弾です。

React NativeはAndroid, iOS, Webで横断的に動作する、それ自体が比較的新しく急速に発展しているクロスプラットフォームフレームワークです。2年間に渡って使ってきた今、React Nativeは多くの点において革新的なツールであると自信を持って言えます。それはモバイル開発におけるパラダイムシフトであり、私たちはReact Nativeが掲げるゴールから多くの恩恵を受けることができました。しかしその利点を得るために多くの痛みが伴ったことは無視できません。

上手くいったこと

クロスプラットフォーム

React Nativeを利用することによる一番の利点は一度書いたコードがAndroidでもiOSでも動作するということです。React Nativeで実装した機能の多くでは95-100%のコードを共有することができ、 プラットフォームに依存したファイル(*.android.js/*.ios.js)は0.2%にすぎませんでした。

共通のデザイン言語システム(DLS)

私たちはDLSと呼ばれるクロスプラットフォームのデザイン言語を開発しており、Android、iOS、React Native、Web、それぞれのバージョンがコンポーネントごとに存在しています。共通のデザイン言語を持つことで一貫したデザイン、コンポーネント名、そして画面を持てるようになり、それによってクロスプラットフォーム機能を書くことが可能となったのです。

しかし同時に必要に応じてプラットフォーム固有のデザインを適用することもまた可能でした。例えばAndroidにおいてToolbarを利用するがiOSではUINavigationBarを使う、Andoroidではプラットフォームのデザインガイドラインに適さないdisclosure indicatorsを隠すなどといったことです。

私たちはネイティブのコンポーネントをラップするのではなく、React Nativeでコンポーネントを書き直す方法を選択しました。なぜならその方が個別にプラットフォーム固有のAPIを叩く上で信頼性が高く、React Native上での変更をテストする手段に詳しくないネイティブエンジニアのメンテナンスにおけるオーバーヘッドを削減できるからです。ただしこのアプローチはネイティブとReact Native間でのバージョンの乖離を引き起こすこととなりました。

React

Reactが最も愛されるWebフレームワークと呼ばれるのには理由があります。シンプルかつ強力で、より大きなコードベースへスケールしやすいからです。特に私たちが気に入っている点としては、

  • Components: 適切に定義されたpropsとstateによってReact Componentは開発における関心の分離を実現します。これはReactのスケーラビリティにおける大きな要因です。
  • 簡潔なライフサイクル: Androidと(多少はマシですが)iOSのライフサイクルは非常に複雑なことで知られています。適切に動作するリアクティブなReact Componentはこの問題を根本的に解決するため、React Nativeの学習をAndroidやiOSのそれと比べて非常に容易にしています。
  • 宣言的: Declarative(宣言的)であるというReactの特徴は、内部のstateとUIの整合性を保つ上でとても役に立ちます。

検証スピード

React Nativeを利用することでアプリケーションの変更を即座に確認できるhot reloadingを活用することができます。ビルドパフォーマンスはわたしたちのネイティブアプリでの最優先課題でしたが、React Nativeで達成したイテレーションスピードに近付くことすらできませんでした。ネイティブのビルドは最速で15秒でしたが、フルビルドで20分かかることもありました。

インフラへの投資

私たちはネイティブのインフラに対して広い範囲でのIntegrationを実現しました。'networking', 'i18n', 'experimentation', 'shared element transitions', 'device info', 'account info'というコア機能をはじめとした多くの機能が個別のReact Native APIとしてラップされました。既にあるAndroidやiOSのAPIをReactのために整合性を持たせたり正規化する必要があったため、それらのBrdigeはとても複雑なパーツとなりました。高速な開発サイクルの中でこれらのBridgeを常に最新の状態に保つ作業が必要になりましたが、インフラチームがこの部分に投資してくれたことによってプロダクト開発がずっと容易になりました。

このインフラ部分への大きな投資がなければ、React Nativeによる開発はもっと中途半端なものなっていたでしょう。React Nativeを既存のアプリにつっこむためには、このような投資が必要不可欠だろうというのが私たちの結論です。

パフォーマンス

React Nativeにまつわる最も大きな心配事の一つがパフォーマンスの問題です。ですが実際はほとんど問題になることはなく、React Nativeで実装した多くの画面がネイティブと遜色なく動作しました。パフォーマンスというものは一面的に捉えられがちです。ネイティブエンジニアがJSに対して「Javaより遅い」と考えているのをよく見かけますが、ビジネスロジックやlayoutをメインスレッドから分割することで描画のパフォーマンスが向上することは多々あります。

パフォーマンスの問題が発生することもありましたが、大抵は過度な描画によるものでshouldComponentUpdateremoveClippedSubviewsを活用したりReduxの使い方を改善することで和らげることができました。

しかしながら初期化処理と最初の描画時間に関するパフォーマンスは低くReact Nativeで作った起動画面やディープリンク、画面回遊時のTTIなどに大きな影響を与えました。それに加えてYogaがReact Native coponentをネイティブのviewに変換するため、スクリーンフレームの低下時にデバッグするのが難しいという問題もありました。

Redux

stateとUIの整合性を保ちやすい、画面をまたいだデータの共有が簡単に行える等の有用性から私たちはReduxを採用しました。ですがReduxは悪名高いboilerplateで知られ、学習の難易度も高いです。一般的なテンプレートを生成するジェネレータも用意しましたが、ReduxはReact Nativeで開発する上で最も難しい部分であり混乱を招きやすい部分であることに変わりはありませんでした。React Nativeに限った問題ではありませんが。

ネイティブの支え

React Nativeの中で起きる全てのことはネイティブのコードにBridgeできるため、開発を始めた時点では実現可能か確信の持てなかった多くのものを最終的に実現することができました。

  1. Shared element transitions: 私たちはAndroidとiOSのネイティブコードと連携した <SharedElement>コンポーネントを開発しました。これはネイティブとReact Nativeのスクリーン間でも利用できます。
  2. Lottie: AndroidとiOS向けのライブラリをラップすることでLottieをReact Native上で動かすことが可能になりました。
  3. ネットワーク機構: React Nativeは既存のわたしたちのネイティブの通信スタックとキャッシュ機構を利用します。
  4. その他のコアインフラ: Networkingだけでなく、'i18n'や'experimentation'のようなその他のネイティブインフラもラップすることでReact Nativeからシームレスに利用できます。

静的解析

AirbnbはWebでのeslintの利用について確固とした実績を持っていますが、prettierを導入したのは私たちのプラットフォームが社内では初めてでした。prettierはPull Requestにおける細かな指摘やミスを軽減するのにとても有効で、Webインフラチームでは活発に試されています。

また描画にかかる時間とコストを解析することで、改修の優先度が高い画面を調べています。

React Nativeは私たちのwebインフラストラクチャよりも新しく小さかったため、新しいアイディアを試すのに持ってこいであることが分かりました。React Nativeのために開発した多くのツールやアイディアが今ではWebにも持ち込まれています。

アニメーション

React Nativeのアニメーションライブラリのおかげで効果的なアニメーションを実装可能になり、スクローリングやパララックスなどインタラクションドリブンなアニメーションも手に入れました。

オープンソース

React NativeではReactとJavaScriptという巨人の肩に乗ることで、redux,reselect, jestといった膨大な数のJSプロジェクトの恩恵を受けることができます。

Flexbox

C言語で書かれたクロスプラットフォームなライブラリでありFlexbox APIによるレイアウト計算を行うYogaをReact Nativeでは利用しています。はじめこそアスペクト比が使えないなどYogaの機能的制限に悩まされましたが、その後のアップデートで解決されました。flebox froggyのような楽しいチュートリアルもオンボーディングで活躍しています。

Webとのコラボレーション

開発が進むにつれて最近ではWeb, iOS, Androidの開発を一度に行えるようになりました。WebでもReduxが使われていることからWebとネイティブで広範囲に渡るコードを、特別な調整なしで共有できることに気が付いたのです。

上手くいかなかったこと

React Nativeの未熟さ故の問題

新しく、野心的で、急速に発展してはいますがReact NativeはAndroidやiOSに比べてまだまだ未成熟なプラットフォームです。多くの状況でReact Nativeは上手く機能する一方、その未熟さが透けて見え、ネイティブではすぐに解決できるような些細な問題がとても難しくなってしまうことがあります。残念ながらこれらのケースは予測が難しく、ワークアラウンドを行うのに数時間で終わることもあれば何日もかかることもあります。

Forked React Nativeのメンテナンス

React Nativeは未成熟であるため場合によってはソースコードに修正を加える必要があります。すぐに修正を反映するためにはReact Native本体にコントリビュートするのみでなく、Forkした独自のレポジトリを用意して管理する必要がありました。この2年間でReact Nativeの上にさらに約50個ほどコミットを積みましたが、これによってReact Nativeのアップデートが恐ろしく大変なものとなったのです。

JavaScriptツール

JavaScriptは動的型付けの言語です。型安全でない言語を扱うことはスケールが難しいだけでなく、型のある世界からやってきたネイティブエンジニアとの争いの元となりました。彼らは型の問題がなければよりReact Nativeの学習に興味を示してくれたでしょう。

私たちはflowの導入を試みましたが、まるで暗号のようなエラーメッセージに開発者のフラストレーションはたまる一方でした。TypeScriptも調査はしましたが、既にあるbabelmetro bundlerとの統合に問題があります。そのような状況ではありますが、今後も私たちはweb向けにTypeScriptの調査を継続していくつもりです。

リファクタリング

JavaScriptという型のない言語を扱った結果、リファクタリングがとても大変な作業になり多くのエラーが吐かれることとなりました。props名の変更、特に複数のコンポーネントにまたがって継承される onClickのような広く使われるpropsを正確に変更する作業はまるで悪夢のようでした。更に悪いことに、そのようなリファクタはコンパイル時でなくプロダクション時にエラーになり、静的解析を適用するのも難しいものでした。

JavaScriptCoreの不安定性

React Nativeのトリッキーな特徴としては、それがJavaScriptCore environmentで実行されるということです。それによって以下のような問題に直面しました。

  • iOSは初期状態で独自のJavaScriptCoreを持っているため動作も概ね安定しており、大きな問題はありませんでした。
  • Androidは自前のJavaScriptCoreを持たないため、React Nativeが自身のものをバンドルすることで実行環境が作られます。問題なのはこのランタイムがとてつもなく古いものであるということです。結果的に私たちは新たに自前で新しいJSCをバンドルしてやる必要がありました。
  • React Nativeはデバッグ時にパワフルなChrome Developer Toolsに接続します。素晴らしい!ですがデバッグモードに入ると同時に、React NativeのコードはChromeのV8エンジンで実行されるようになります。99.9%のケースでは特に問題はないのですが、ただ一度だけ、 toLocaleStringメソッドがAndroidではデバッグモードでしか動かないという問題に引っかかりました(iOSは大丈夫でした)。どうやらAndroidのJavaScriptCoreにメソッドが含まれておらず、そこでエラーも吐かずに落ちていたようです。当然デバッグ時に利用するV8環境では動作していました。このような問題に対処できる深い技術的知識がなければ、何日も現場のエンジニア達は悩まされることになるでしょう。

React Native向けのオープンソースライブラリ

任意のプラットフォームについて学び、習熟するのには長い時間がかかります。多くの人は1つまたは2つのプラットフォームについてのみしか理解してないと言えるでしょう。mapやvideoといったネイティブとのbrdigeを持ったReact Nativeライブラリを開発するには3つ全てのプラットフォーム知識が求められます。残念ながら多くのReact Native向けオープンソースライブラリの開発者はそのうちの1つか2つのプラットフォームしか経験していませんでした。それはAndroidもしくはiOSにおける不整合や予期しないバグを引き起こしました。

Android向けの多くのReact Nativeライブラリではパッケージの読み込みにmavenではなくnode modulesの相対パスによる記述を要求しますが、これはコミュニティの期待する方法とは一貫しないやりかたです。

インフラの二重管理と機能開発

私たちは長年に渡ってAndroidとiOSにおけるネイティブのインフラストラクチャを構築・蓄積してきましたが、React Nativeに向けて全ての既存のインフラに対するBridgeをゼロから作成する必要がありました。つまりエンジニアが新しい機能を開発する際に、よく理解していないプラットフォームに対して自らのプロジェクトのスコープ外の作業を行ってbridgeを作るか、それが作れるようになるまで待つ必要があったのです。

クラッシュレポート

私たちはネイティブランタイムでのクラッシュレポートにBugsnagを利用しています。AndroidとiOSのどちらのプラットフォームでも大抵は動作する一方で、あまり信頼性は高いとは言えず、また他のプラットフォームに比べて多くの作業が必要でした。React Nativeは新興であるが故に、私たち自身の手でsource mapアップロードのようなインフラを大量に作成したり、BugsnagがReact Nativeで起きた問題のみをトラックできるようなフィルタリング機能を追加する必要がありました。

このように多くのインフラレベルの機能をReact Nativeに実装しなければいけない状況下であるため、クラッシュしたのにレポートが送信されない、source mapが適切にアップロードされないといった問題に悩まされることになりました。

もう一つ付け加えると、ネイティブとReact Native間でスタックトレースは別れてしまうため、双方を跨いだ問題のデバッグはとても大変なものになるでしょう。

ネイティブブリッジ

React Nativeにはネイティブと通信するためのbridge APIがあります。このAPIは期待通りの動作をしてくれるものの、実装するのがものすごく面倒です。まず初めに3つのプラットフォーム向けのそれぞれの開発環境を適切に設定する必要があります。JavaScriptから返ってくるデータ型が予測不可能という問題もあります。例えばinteger型はしばしばstring型にラップされてしまい、bridgeを通ってみるまでどうなるのか分かりません。またAndroidではクラッシュするのにiOSではフィードバックもなく落ちることがあります。私たちは2017年の終わりにかけてTypeScriptの型定義ファイルからbridgeコードを自動生成する方法に取り組みましたが、それはかけるコストも少なすぎ、タイミングも遅すぎました。

初期化の時間

React Nativeがファーストビューを描画するには、まずその前にランタイムを初期化する必要があります。残念ながら私たちほどの規模になると、たとえそれがハイエンドデバイスであっても数秒かかってしまいます。このような問題があるなかでReact Nativeをアプリの起動画面に採用することはほぼ不可能です。私たちはアプリの起動時にReact Nativeの初期化を行うことでファーストビューの描画にかかる時間を最小化しました。

ファーストビューの描画時間

ネイティブの場合とは異なりReact Nativeでファーストビューに必要な情報をあつめて描画するためには、メインスレッド -> JS -> yogaレイアウトスレッド -> メインスレッドというサイクルを実行する必要があります。ファーストビューを描画するまでの時間の90パーセンタイルの平均はiOSで280ms、Androidで440msでした。AndroidではpostponeEnterTransition APIを利用しています。通常はshared element transitionにおいて描画までのディレイを適用するために使われるものです。iOSにおいてはNavBarの設定にReact Nativeだと時間がかかりすぎるという問題がありました。最終的に私たちはnavbarの設定が読み込まれるまではインタラクションが発生しないよう、全てのReact Nativeでのページ遷移に50msのディレイをかけることにしました。

App Size

React Nativeによるアプリケーションサイズへの影響も無視できません。Androidの場合、React Nativeのトータルのサイズ(Java + JS + Yoga含むネイティブライブラリ + JavaScript Runtime)はABIごとに8MBとなりました。x86とarm(32bit only)を含めたAPKなら12MBほどになるでしょう。

64bit

この問題により、今の所はまだAndroidに向けて64bitのAPKを配信できていません。

ジェスチャー

タッチコントロールに関するサブシステムはAndroidとiOS間での違いが大きすぎることもあり、それらを統合するAPIを開発することはReact Nativeコミュニティ全体にとって大きな課題です。そのようなこともあり現在Airbnbでは、複雑なジェスチャーを要求する画面ではReact Nativeは利用していません。しかしながらreact-native-gesture-handlerのv1.0が最近リリースされるなど、まだ取り組みは活発なようです。

リスト表示

FlatListのようなライブラリが登場したことでReact Nativeでもこの分野が発展してきているようです。ですがそれはまだAndroidのRecyclerViewやiOSのUICollectionViewには到底及びません。スレッドの機構により多くの制限がなかなか解決されないでいます。Adapterのデータには同期的にアクセスできないので、素早くスクロールするとそれらが非同期的にレンダリングされて画面がちらつく可能性があります。テキストもiOSでは非同期で計測されるため、事前にCellの高さを計算することが難しいです。

(* 訳注: あまり意味が理解できておらず申し訳ないです。)

React Nativeのアップグレード

React Nativeにおけるほとんどのアップグレード作業は難しくありませんが、とても大変なケースがいくつかありました。具体的な例を挙げるとReact Native 0.43(2017年4月リリース)からReact Native 0.49(2017年10月リリース)へのアップグレード作業では、前者がReact 16のα版、後者がβ版を利用していた関係でほぼ不可能とも思える作業になりました。多くのReactライブラリがWebを想定して設計されているため、最新版のReactへの対応がなされておらず手こずることとなりました。2017年中頃のインフラチームはこの依存解決の問題に追われ、結果的に多くのコストが費やされたこととなります。

アクセシビリティ

2017年に私たちはアクセシビリティに関する大幅な改修を行い、ハンディキャップを抱えた人たちが最適なリスティングを見つけ、予約できるように努力しました。しかしながらReact NativeのアクセシビリティAPIにはいくつもの穴があります。最低限のアクセシビリティ要件を満たすためだけでも、私たちはForkしたReact Nativeをメンテナンスして修正をマージする必要がありました。AndroidやiOSならば1行加えるだけですむような修正でも、React Nativeでの実装方法を考え、変更を加え、React Native Coreに修正を出してケアするのには膨大な時間がかかりました。

やっかいなクラッシュ

私たちはいくつかの修正が難しい奇妙な問題にもぶつかりました。例をあげるとこちらの@ReactPropに関する問題を現在も調査中で、同じ端末同じソフトウェア上であっても問題を再現・究明できずにいます。

Androidでのプロセスの永続化

Androidは頻繁にバックグラウンドプロセスをリフレッシュしますが、その対策としてアプリケーションバンドルにstateを同期的に保存することが可能です。しかしながらReact Nativeにおいては全てのstateに対してJSスレッドからしかアクセスできないため、これを同期的に行うことができません。もしできたとしてもReduxにはシリアライズ可能なデータと不可能なデータが混在していますし、savedInstanceStateの容量では足りない大きさのデータも含まれるため、どちらにせよプロダクションでクラッシュしてしまいます。

その他の記事