パフォーマンス概要
WebViewベースのツールではなくReact Nativeを使用する説得力のある理由は、少なくとも60フレーム/秒を達成し、アプリにネイティブのルックアンドフィールを提供することです。可能な限り、React Nativeが自動的に最適化を処理し、開発者がパフォーマンスを気にすることなくアプリに集中できるようにすることを目指しています。しかし、まだそのレベルに達していない領域や、React Nativeが(ネイティブコードを直接書くのと同様に)あなたにとって最善の最適化アプローチを判断できない領域もあります。そのような場合には、手動での介入が必要になります。私たちはデフォルトでバターのように滑らかなUIパフォーマンスを提供することを目指していますが、それが不可能な場合もあります。
このガイドは、パフォーマンス問題のトラブルシューティングに役立ついくつかの基本を教えること、そしてパフォーマンス問題の一般的な原因とその解決策について議論することを目的としています。
フレームについて知っておくべきこと
あなたの祖父母の世代が映画を「活動写真 (moving pictures)」と呼んだのには理由があります。ビデオのリアルな動きは、静止画を一定の速度で素早く切り替えることによって作られる錯覚です。私たちはこれらの各画像をフレームと呼びます。1秒間に表示されるフレーム数は、ビデオ(またはユーザーインターフェース)がどれだけ滑らかで、最終的にどれだけリアルに見えるかに直接影響します。iOSおよびAndroidデバイスは少なくとも毎秒60フレームを表示します。これは、あなたとUIシステムが、その間隔でユーザーが画面上で見る静止画(フレーム)を生成するために必要なすべての作業を行うのに、最大でも16.67ミリ秒しかないことを意味します。割り当てられた時間内にそのフレームを生成するために必要な作業ができない場合、「フレームをドロップ」したことになり、UIは反応しないように見えます。
さて、少し問題を複雑にしますが、アプリの開発者メニューを開き、Show Perf Monitor
を切り替えてみてください。2つの異なるフレームレートがあることに気づくでしょう。
JSフレームレート(JavaScriptスレッド)
ほとんどのReact Nativeアプリケーションでは、ビジネスロジックはJavaScriptスレッド上で実行されます。ここにあなたのReactアプリケーションが存在し、API呼び出しが行われ、タッチイベントが処理されます。ネイティブに裏付けられたビューへの更新はバッチ処理され、イベントループの各イテレーションの最後に、フレームの期限前に(すべてがうまくいけば)ネイティブ側に送られます。もしJavaScriptスレッドがあるフレームで応答しなくなると、それはドロップされたフレームと見なされます。たとえば、複雑なアプリケーションのルートコンポーネントで新しいstateを設定し、それが計算コストの高いコンポーネントのサブツリーの再レンダリングを引き起こした場合、これに200msかかり、結果として12フレームがドロップされることも考えられます。JavaScriptによって制御されるアニメーションは、その間フリーズしたように見えるでしょう。十分なフレームがドロップされると、ユーザーはそれを体感します。
一つの例はタッチへの応答です。もしJavaScriptスレッド上で複数のフレームにまたがる作業を行っている場合、例えばTouchableOpacity
への応答に遅延があることに気づくかもしれません。これはJavaScriptスレッドがビジー状態であり、メインスレッドから送られてくる生のタッチイベントを処理できないためです。その結果、TouchableOpacity
はタッチイベントに反応してネイティブビューに不透明度を調整するよう命令することができません。
UIフレームレート(メインスレッド)
React Navigationが提供する@react-navigation/native-stackのようなネイティブスタックナビゲーターのパフォーマンスが、JavaScriptベースのスタックナビゲーターよりも初期状態で優れていることにお気づきかもしれません。これは、遷移アニメーションがネイティブのメインUIスレッドで実行されるため、JavaScriptスレッドでのフレームドロップによって中断されないためです。
同様に、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
の使用方法については、Animationsガイドを参照してください。
注意点
LayoutAnimation
は「静的な」アニメーション、つまり一度実行したらそのままのアニメーションにしか機能しません。中断可能である必要がある場合は、Animated
を使用する必要があります。
画面上でビューを動かす(スクロール、移動、回転)とUIスレッドのFPSが低下する
これは特にAndroidで、画像の上に透明な背景を持つテキストを配置した場合や、各フレームでビューを再描画するためにアルファ合成が必要となる他の状況で顕著です。renderToHardwareTextureAndroid
を有効にすると、これが大幅に改善されることがわかります。iOSの場合、shouldRasterizeIOS
はデフォルトですでに有効になっています。
これを使いすぎるとメモリ使用量が急増する可能性があるので注意してください。これらのプロパティを使用する際は、パフォーマンスとメモリ使用量をプロファイリングしてください。ビューをこれ以上動かす予定がない場合は、このプロパティをオフにしてください。
画像のサイズをアニメーションさせるとUIスレッドのFPSが低下する
iOSでは、Image
コンポーネントの幅や高さを調整するたびに、元の画像から再クロップおよびスケーリングされます。これは特に大きな画像の場合、非常にコストが高くなる可能性があります。代わりに、transform: [{scale}]
スタイルプロパティを使用してサイズをアニメーションさせてください。これを行うかもしれない例としては、画像をタップしてフルスクリーンに拡大表示する場合などがあります。
TouchableXビューの反応が悪い
タッチに反応しているコンポーネントの不透明度やハイライトを調整するのと同じフレームでアクションを実行すると、onPress
関数が返されるまでその効果が見られないことがあります。これは、onPress
が重い再レンダリングを引き起こすstateを設定し、その結果いくつかのフレームがドロップされた場合に発生する可能性があります。この解決策は、onPress
ハンドラ内のアクションをrequestAnimationFrame
でラップすることです。
function handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}