メインコンテンツにスキップ

パフォーマンス概要

WebViewベースのツールではなくReact Nativeを使用する説得力のある理由は、少なくとも毎秒60フレームを実現し、アプリにネイティブのルックアンドフィールを提供することです。可能な限り、React Nativeは自動的に最適化を処理することを目指しており、パフォーマンスを心配することなくアプリに集中できます。ただし、まだそのレベルに達していない領域や、React Native(ネイティブコードを直接記述する場合と同様)が最適な最適化アプローチを判断できない領域があります。このような場合は、手動による介入が必要になります。デフォルトでは、非常にスムーズなUIパフォーマンスを提供するよう努めていますが、それが不可能な場合があります。

このガイドでは、パフォーマンスの問題のトラブルシューティングに役立つ基本的な知識と、一般的な問題の原因とその解決策について説明します。

フレームについて知っておくべきこと

あなたの祖父母の世代は、映画を"動画"と呼んでいました。それは、ビデオのリアルな動きは、静止画像を一定の速度で高速に変化させることで生み出される錯覚だからです。これらの画像をそれぞれフレームと呼びます。毎秒表示されるフレーム数は、ビデオ(またはユーザーインターフェイス)がどれほどスムーズで、最終的にどれほどリアルに見えるかに直接影響します。 iOSデバイスは毎秒少なくとも60フレームを表示します。そのため、ユーザーインターフェイスシステムは、その間隔でユーザーが画面に表示する静止画像(フレーム)を生成するために必要なすべての作業を最大16.67ミリ秒で行う必要があります。割り当てられた時間内にそのフレームを生成するために必要な作業を実行できない場合、「フレームをドロップ」し、UIが応答しなくなります。

ここで少し話をややこしくしますが、アプリで開発メニューを開き、`パフォーマンスモニタの表示`を切り替えます。 2つの異なるフレームレートがあることに気付くでしょう。

JSフレームレート(JavaScriptスレッド)

ほとんどのReact Nativeアプリケーションでは、ビジネスロジックはJavaScriptスレッドで実行されます。これは、Reactアプリケーションが存在し、API呼び出しが行われ、タッチイベントが処理される場所などです。ネイティブでサポートされるビューの更新はバッチ処理され、イベントループの各イテレーションの最後に、フレームの期限前に(すべてがうまくいけば)ネイティブ側に送信されます。 JavaScriptスレッドがフレームに対して応答しない場合、ドロップされたフレームと見なされます。たとえば、複雑なアプリケーションのルートコンポーネントで`this.setState`を呼び出し、計算コストの高いコンポーネントサブツリーの再レンダリングが発生した場合、これが200ミリ秒かかり、12フレームがドロップされる可能性があります。 JavaScriptによって制御されるアニメーションは、その間フリーズしたように見えます。何かが100ミリ秒以上かかる場合、ユーザーはそれを体感します。

これは、多くの場合、`Navigator`の遷移中に発生します。新しいルートをプッシュすると、JavaScriptスレッドは、バッキングビューを作成するための適切なコマンドをネイティブ側に送信するために、シーンに必要なすべてのコンポーネントをレンダリングする必要があります。ここで行われている作業に数フレームかかり、遷移がJavaScriptスレッドによって制御されるため、ジャンクが発生することは珍しくありません。コンポーネントが`componentDidMount`で追加の作業を行うこともあり、遷移に2回目の途切れが発生する可能性があります。

別の例はタッチへの応答です。 JavaScriptスレッドで複数のフレームにわたって作業を行っている場合、たとえば`TouchableOpacity`への応答に遅延が発生することがあります。これは、JavaScriptスレッドがビジー状態で、メインスレッドから送信された未処理のタッチイベントを処理できないためです。その結果、`TouchableOpacity`はタッチイベントに反応できず、ネイティブビューに不透明度を調整するように命令できません。

UIフレームレート(メインスレッド)

多くの人が、`NavigatorIOS`のパフォーマンスが`Navigator`よりもすぐに優れていることに気づいています。これは、遷移のアニメーションがメインスレッドで完全に実行されるため、JavaScriptスレッドのフレームドロップによって中断されないためです。

同様に、`ScrollView`はメインスレッドに存在するため、JavaScriptスレッドがロックされている場合でも、`ScrollView`を上下にスクロールできます。スクロールイベントはJSスレッドにディスパッチされますが、スクロールが発生するためにそれらの受信は必要ありません。

パフォーマンスの問題の一般的な原因

開発モード(`dev=true`)での実行

開発モードで実行すると、JavaScriptスレッドのパフォーマンスが大幅に低下します。これは避けられません。適切な警告とエラーメッセージを提供するために、実行時にさらに多くの作業を行う必要があります。常にリリースビルドでパフォーマンスをテストしてください。

`console.log`ステートメントの使用

バンドルされたアプリを実行する場合、これらのステートメントはJavaScriptスレッドの大きなボトルネックになる可能性があります。これには、redux-loggerなどのデバッグライブラリからの呼び出しが含まれるため、バンドルする前に必ず削除してください。また、すべての`console.*`呼び出しを削除するこのbabelプラグインを使用することもできます。最初に`npm i babel-plugin-transform-remove-console --save-dev`でインストールしてから、プロジェクトディレクトリの下にある`.babelrc`ファイルを次のように編集します

{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}

これにより、プロジェクトのリリース(本番)バージョンですべての`console.*`呼び出しが自動的に削除されます。

プロジェクトで`console.*`呼び出しが行われていない場合でも、プラグインを使用することをお勧めします。サードパーティのライブラリもそれらを呼び出す可能性があります。

`ListView`の初期レンダリングが遅すぎる、または大きなリストのスクロールパフォーマンスが悪い

代わりに、新しい`FlatList`または`SectionList`コンポーネントを使用してください。 APIの簡素化に加えて、新しいリストコンポーネントには、パフォーマンスの大幅な向上があり、主なものは、任意の数の行に対してほぼ一定のメモリ使用量です。

`FlatList`のレンダリングが遅い場合は、`getItemLayout`を実装して、レンダリングされたアイテムの測定をスキップすることでレンダリング速度を最適化していることを確認してください。

ほとんど変化しないビューを再レンダリングするとJS FPSが急落する

ListViewを使用している場合は、行を再レンダリングする必要があるかどうかを迅速に判断することで多くの作業を削減できる`rowHasChanged`関数を指定する必要があります。不変のデータ構造を使用している場合、これは参照の等価チェックのみである必要があります。

同様に、`shouldComponentUpdate`を実装し、コンポーネントを再レンダリングする正確な条件を示すことができます。純粋なコンポーネント(render関数の戻り値が完全にpropsとstateに依存している)を記述する場合は、PureComponentを活用してこれを行うことができます。繰り返しますが、不変のデータ構造はこれを高速に保つために役立ちます。オブジェクトの大きなリストを詳細に比較する必要がある場合、コンポーネント全体を再レンダリングする方が速く、コードも少なくて済みます。

JavaScriptスレッドで同時に多くの作業を行うため、JSスレッドFPSが低下する

「遅いナビゲーターの遷移」は、これの最も一般的な兆候ですが、これが発生する可能性のある他の時間もあります。 InteractionManagerを使用することは良いアプローチですが、アニメーション中に作業を遅らせることによるユーザーエクスペリエンスのコストが高すぎる場合は、LayoutAnimationを検討することをお勧めします。

Animated APIは現在、`useNativeDriver:true`を設定しない限り、JavaScriptスレッドでオンデマンドで各キーフレームを計算しますが、LayoutAnimationはCore Animationを活用し、JSスレッドとメインスレッドのフレームドロップの影響を受けません。

私がこれを使用した1つのケースは、モーダルでアニメーションを実行する(上からスライドダウンして半透明のオーバーレイでフェードインする)と同時に、いくつかのネットワークリクエストを初期化して応答を受信し、モーダルのコンテンツをレンダリングし、モーダルが開かれたビューを更新する場合です。 LayoutAnimationの使用方法の詳細については、アニメーションガイドを参照してください。

注意事項

  • LayoutAnimationは、fire-and-forgetアニメーション(「静的」アニメーション)に対してのみ機能します。中断可能にする必要がある場合は、`Animated`を使用する必要があります。

画面上のビューの移動(スクロール、平行移動、回転)により、UI スレッドの FPS が低下する

これは、画像の上に透明な背景を持つテキストを配置した場合、または各フレームでビューを再描画するためにアルファ合成が必要な場合に特に当てはまります。shouldRasterizeIOS または renderToHardwareTextureAndroid を有効にすると、これが大幅に改善されます。

これらを使いすぎるとメモリ使用量が急増する可能性があるため、注意してください。これらのプロパティを使用する際は、パフォーマンスとメモリ使用量をプロファイリングしてください。ビューをこれ以上移動しない場合は、このプロパティをオフにしてください。

画像サイズのアニメーション化により、UI スレッドの FPS が低下する

iOS では、Image コンポーネントの幅または高さを調整するたびに、元の画像から再トリミングおよびスケーリングされます。これは、特に大きな画像の場合、非常にコストがかかります。代わりに、transform: [{scale}] スタイルプロパティを使用してサイズをアニメーション化してください。たとえば、画像をタップしてフルスクリーンに拡大する場合などです。

TouchableX ビューの応答性が低い

タッチに応答しているコンポーネントの不透明度またはハイライトを調整しているのと同じフレームでアクションを実行すると、onPress 関数が戻るまでその効果が表示されない場合があります。onPress が大量の処理とフレームのドロップを引き起こす setState を実行する場合、これが発生する可能性があります。この解決策は、onPress ハンドラー内のすべてのアクションを requestAnimationFrame でラップすることです。

handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}

ナビゲーターの遷移が遅い

上記のように、Navigator アニメーションは JavaScript スレッドによって制御されます。「右からプッシュ」シーン遷移を想像してみてください。各フレームで、新しいシーンは画面外(x オフセット 320 など)から開始し、最終的にシーンが x オフセット 0 に配置されたときに停止します。この遷移中の各フレームで、JavaScript スレッドは新しい x オフセットをメインスレッドに送信する必要があります。JavaScript スレッドがロックされている場合、これは実行できず、そのフレームでは更新が発生せず、アニメーションが途切れます。

この解決策の 1 つは、JavaScript ベースのアニメーションをメインスレッドにオフロードできるようにすることです。このアプローチで上記の例と同じことを行う場合、遷移を開始するときに新しいシーンのすべての x オフセットのリストを計算し、それらをメインスレッドに送信して最適化された方法で実行することができます。これで JavaScript スレッドはこの責任から解放されたため、シーンのレンダリング中に数フレームがドロップしても大きな問題ではありません。おそらく、きれいな遷移に気を取られて気づかないでしょう。

これを解決することは、新しい React Navigation ライブラリの主な目標の 1 つです。React Navigation のビューは、ネイティブコンポーネントと Animated ライブラリを使用して、ネイティブスレッドで実行される少なくとも 60 FPS のアニメーションを提供します。