パフォーマンス概要
WebViewベースのツールではなくReact Nativeを使用する説得力のある理由は、少なくとも毎秒60フレームを達成し、アプリにネイティブなルックアンドフィールを提供することです。可能な限り、React Nativeが最適化を自動的に処理し、パフォーマンスを気にせずにアプリに集中できるようにすることを目指しています。ただし、まだそのレベルに達していない領域や、React Native(ネイティブコードを直接記述するのと同様に)が最適な最適化アプローチを決定できない領域もあります。そのような場合、手動での介入が必要になります。デフォルトで非常にスムーズなUIパフォーマンスを提供できるよう努めていますが、それが不可能な場合もあります。
このガイドは、パフォーマンス問題のトラブルシューティングに役立ついくつかの基本を教えることを目的としています。また、一般的な問題の原因とその解決策についても説明します。
フレームについて知っておくべきこと
祖父母の世代が映画を「動く絵」と呼んだのには理由があります。ビデオにおけるリアルな動きは、静止画を一定の速度で素早く切り替えることによって生じる幻想です。これらの画像をそれぞれフレームと呼びます。1秒間に表示されるフレーム数は、ビデオ(またはユーザーインターフェース)がどれだけ滑らかで、最終的にリアルに見えるかに直接影響します。iOSおよびAndroidデバイスは少なくとも毎秒60フレームを表示するため、ユーザーがその間画面上で見る静止画(フレーム)を生成するために必要なすべての作業を行うのに、あなたとUIシステムには最大16.67msが与えられます。割り当てられた時間内にそのフレームを生成するために必要な作業ができない場合、あなたは「フレームを落とし」、UIは応答しないように見えます。
少し分かりにくくなるかもしれませんが、アプリで開発者メニューを開き、Show Perf Monitorを切り替えてみてください。2つの異なるフレームレートがあることに気づくでしょう。

JSフレームレート (JavaScriptスレッド)
ほとんどのReact Nativeアプリケーションでは、ビジネスロジックはJavaScriptスレッドで実行されます。ここには、Reactアプリケーションが動作し、API呼び出しが行われ、タッチイベントが処理されます。ネイティブバックのビューへの更新はバッチ処理され、フレームデッドライン前に(すべてがうまくいけば)イベントループの各イテレーションの最後にネイティブ側に送信されます。JavaScriptスレッドがフレームに対して応答しない場合、それはドロップされたフレームと見なされます。たとえば、複雑なアプリケーションのルートコンポーネントに新しい状態を設定し、それが計算コストの高いコンポーネントサブツリーの再レンダリングを引き起こした場合、これが200msかかり、12フレームがドロップされる可能性があります。JavaScriptによって制御されるアニメーションはその間フリーズしているように見えます。十分なフレームがドロップされると、ユーザーはそれを感じます。
例として、タッチへの応答があります。JavaScriptスレッドで複数のフレームにわたって作業を行っている場合、TouchableOpacityへの応答に遅延があることに気付くかもしれません。これは、JavaScriptスレッドがビジーで、メインスレッドから送られてくる生のタッチイベントを処理できないためです。結果として、TouchableOpacityはタッチイベントに反応できず、ネイティブビューにその不透明度を調整するよう命令できません。
UIフレームレート (メインスレッド)
ネイティブスタックナビゲーター(React Navigationが提供する@react-navigation/native-stackなど)のパフォーマンスが、JavaScriptベースのスタックナビゲーターよりもすぐに優れていることに気づいたかもしれません。これは、遷移アニメーションがネイティブのメインUIスレッドで実行されるため、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.*の呼び出しがなくても、このプラグインを使用することをお勧めします。サードパーティのライブラリもそれらを呼び出す可能性があります。
FlatListのレンダリングが遅すぎる、または大規模なリストのスクロールパフォーマンスが悪い
FlatListのレンダリングが遅い場合は、レンダリングされるアイテムの計測をスキップしてレンダリング速度を最適化するために、getItemLayoutを実装していることを確認してください。
パフォーマンスのために最適化された他のサードパーティリストライブラリもあります。これには、FlashListやLegend Listが含まれます。
JavaScriptスレッドで同時に多くの作業を行うことによるJSスレッドFPSの低下
「ナビゲーター遷移が遅い」が最も一般的な症状ですが、これが発生する他のケースもあります。InteractionManagerを使用するのは良いアプローチかもしれませんが、アニメーション中に作業を遅らせることによるユーザーエクスペリエンスのコストが高すぎる場合は、LayoutAnimationを検討したいかもしれません。
Animated APIは、useNativeDriver: trueを設定しない限り、各キーフレームをJavaScriptスレッドでオンデマンドで計算しますが、LayoutAnimationはCore Animationを利用しており、JSスレッドやメインスレッドのフレーム落ちの影響を受けません。
これを使用するケースの1つは、モーダル(上からスライドして半透明のオーバーレイがフェードインする)をアニメーション表示しながら、複数のネットワークリクエストの初期化や応答の受信、モーダルコンテンツのレンダリング、モーダルが開かれたビューの更新を行う場合です。LayoutAnimationの使用方法の詳細については、アニメーションガイドを参照してください。
注意点
LayoutAnimationは、発火して忘れるタイプのアニメーション(「静的」アニメーション)にのみ機能します。中断可能でなければならない場合は、Animatedを使用する必要があります。
画面上のビューを移動(スクロール、移動、回転)するとUIスレッドのFPSが低下する
これは特にAndroidで顕著であり、画像の上に透明な背景のテキストを配置している場合や、各フレームでビューを再描画するためにアルファ合成が必要なその他の状況で発生します。renderToHardwareTextureAndroidを有効にすると、これが大幅に改善されることがわかります。iOSの場合、shouldRasterizeIOSはデフォルトで既に有効になっています。
これを過剰に使用すると、メモリ使用量が大幅に増加する可能性があるため注意してください。これらのプロパティを使用する際は、パフォーマンスとメモリ使用量をプロファイリングしてください。ビューをこれ以上移動しない場合は、このプロパティをオフにしてください。
画像のサイズをアニメーションするとUIスレッドのFPSが低下する
iOSでは、Imageコンポーネントの幅または高さを調整するたびに、元の画像から再度トリミングおよびスケーリングされます。これは、特に大きな画像の場合、非常にコストが高くなる可能性があります。代わりに、transform: [{scale}]スタイルプロパティを使用してサイズをアニメーション化してください。これを行う場合の例としては、画像をタップしてフルスクリーンに拡大する場合などがあります。
私のTouchableXビューはあまり応答性が良くない
タッチに反応するコンポーネントの不透明度やハイライトを調整している同じフレームでアクションを実行すると、onPress関数が返されるまでその効果が表示されない場合があります。これは、onPressが大規模な再レンダリングを引き起こす状態を設定し、その結果いくつかのフレームがドロップされた場合に発生する可能性があります。これに対する解決策は、onPressハンドラ内の任意のアクションをrequestAnimationFrameでラップすることです。
function handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}