アプリのパフォーマンスに関するベストプラクティス
Vega向けReact NativeアプリはReact Nativeアプリであるため、React Nativeのベストプラクティスに関する公開ドキュメントが同じように適用されます。ここでは、Vega向けReact Nativeアプリを構築するときに考慮する必要のある主なベストプラクティスを、KeplerとiOSやAndroidとの主な違いを交えて説明します。
再レンダリングと再定義の回避
memo
Reactのmemoを使用すると、関数コンポーネントの不要な再レンダリングを防ぎ、パフォーマンを向上させることができます。デフォルトでは、Reactは親コンポーネントが再レンダリングされるたびにコンポーネントを再レンダリングします。これはコンポーネントのプロパティが変更されていなくても行われます。Reactのmemoは、以前のプロパティと新しいプロパティの浅い比較を実行して、等しい場合は再レンダリングをスキップすることでこの動作を最適化します。これは、リストアイテムやボタンなど、頻繁に更新する必要のない安定したプロパティを持つコンポーネントや、レンダリングロジックの負荷が高いUI要素に対して効果的です。memoをuseCallbackおよびuseMemoと組み合わせることで、無駄なレンダリングを最小限に抑え、Reactアプリの効率を高めることもできます。
デフォルトでは、memoはObject.is静的メソッドを使用して、プロパティの浅い比較を実行し、コンポーネントの再レンダリングが必要かどうかを判断します。場合によっては、この比較をカスタマイズするために、独自のarePropsEqual関数を用意することが必要になります。よくあるユースケースは、インラインスタイルや匿名で定義されたプロパティ値を関数コンポーネントに渡す場合です。この場合、プロパティの値としては等しくてもメモリ内では等しくないため、Object.is()による比較は等しくないという結果になります。複雑なオブジェクトや関数をプロパティとして扱っていて、浅い比較では不十分な場合は、比較のカスタマイズが役立ちます。カスタム比較関数を使用すると、コンポーネントを更新するタイミングをより細かく制御でき、深い比較や固有の条件が必要なシナリオでのパフォーマンスが向上します。
Reactコンポーネントの再レンダリングを調査するために役立つツールとして、react-devtoolsプロファイラー(英語のみ)の [Record why each component rendered while profiling] フラグと、コミュニティパッケージのwhy-did-you-render(WDYR)(英語のみ)の2つがあります。
一般に、ユーザー定義の関数コンポーネントはmemo APIでラップすることが最善です。ただし、あらゆる場所でmemoとuseMemoを使用することが効果的とは限りません。memoとuseMemoをいつどのように使用するかについては、Reactの記事の「あらゆる場所にmemoを追加すべきか?」を参照してください。
Reactコンパイラとeslint-plugin-react-compiler
React 19リリース候補版(RC)では、memo、useMemo、useCallbackを通じて自動的にメモ化を適用する静的Reactコードコンパイラが導入されました。コンパイラは、アプリがReactのルールに従っていること、セマンティックなJavaScriptまたはTypeScriptであること、null許容または省略可能な値やプロパティへのアクセス前にはテストしていることを前提としています。Reactによれば「コンパイラは実験的であり、多くの粗削りな部分がある」ということですが、Amazonでは、ReactコンパイラのESLintプラグインであるeslint-plugin-react-compilerにオンボーディングすることを推奨します。これにはReact 19へのアップグレードは必要ありません。オンボーディングするには、eslint-plugin-react-hooksのインストールを参照してください。
Strictモード
React Nativeアプリを<StrictMode>でラップすると、開発中にアプリ内の潜在的な問題を特定するために役立ちます。<StrictMode>は追加のチェックを実行し、非推奨のライフサイクルメソッド、安全でない副作用、パフォーマンスの問題やバグにつながる可能性のあるその他の潜在的な問題について警告します。また、意図しない副作用がないことを確認するために、コンポーネントコンストラクターやuseEffectコールバックなどの特定の関数を二重に呼び出します。このデバッグツールは、開発者が問題を早期に捕捉するために役立ち、アプリの安定性と効率性の改善につながります。<StrictMode>は本番環境のビルドには影響しませんが、開発中に使用することでベストプラクティスへの準拠が確保され、将来のReactやReact Nativeのアップデートに対応できるようにアプリが準備されます。
Suspenseとlazy
Reactのlazyコンポーネントと<Suspense>境界を使用すると、起動時間とアプリ全体のパフォーマンスを大幅に向上させることができます。Reactのlazyは、コード分割を可能にして、コンポーネントの読み込みが必要時にのみ行われるようにします。これにより、初期バンドルサイズが削減され、アプリの初回レンダリングが高速化されます。Reactの<Suspense>は、この動作と連携するために、必要なコンポーネントが読み込まれるまでアプリがレンダリングを「一時停止」できるようにします。これにより、UIを妨げないスムーズなユーザーエクスペリエンスを作成できます。このアプローチでは、アプリの起動時間が最適化されるだけでなく、バンドルの読み込みも効率化され、ビューごとに必要なコードのみが読み込まれます。
useCallback
ReactでuseCallbackを使用すると、関数のインスタンスがメモ化され、レンダリングごとの不要な再作成が防止されるため、パフォーマンスが向上します。useCallbackは、関数をプロパティとして子コンポーネントに渡す場合に効果的です。それらのコンポーネントが参照等価性チェックに依存している場合に、不要な再レンダリングを回避するために役立ちます。たとえば、React.memoでラップされている場合が該当します。useCallbackは、関数がエフェクトの依存関係である場合(useEffect)にも役立ち、副作用が意図せず再実行されるのを防ぐことができます。useCallbackは、過度に使用するとメモリ使用量と複雑さが増す可能性があるため、慎重に使用してください。useCallbackを正しく使用すると、Reactアプリのレンダリング効率が最適化され、パフォーマンスのオーバーヘッドが削減されます。
useCallbackの依存配列には項目を追加して、メモ化された関数が、参照しているリアクティブな依存関係の最新の値にアクセスできるようにする必要があります。依存関係を省略すると、クロージャが古くなり、以前のレンダリングからの古い値が関数で使用され続けて、予期しない動作やバグが発生する可能性があります。
useEffect()
React Nativeでの開発に限りませんが、ReactのuseEffectをどのように使用するかがパフォーマンスに影響する可能性があります。Reactの記事の「そのエフェクトは不要かも」が参考になります。useEffectの依存配列には常に、すべてのリアクティブな値(コンポーネントの本体で宣言されているプロパティ、状態、変数、関数など)を含めるようにしてください。eslint-plugin-react-compilerを使用すると、useEffectの依存配列に不足している依存関係を検出できます。
useMemo
ReactのuseMemoは、コストの高い計算をメモ化し、レンダリングのたびに不要な再計算が行われるのを防ぐことでパフォーマンスを高めます。useMemoを使用しない場合は、複雑な操作(大規模なリストのフィルタリング、数学的な計算の実行、データの処理など)を行う関数が、レンダリングのたびに依存関係に変更がなくても実行されます。このような計算をuseMemoでラップすると、Reactは、その依存関係が更新されたときにのみ再評価を実行します。これにより、CPU使用率が低減され、応答性が向上します。これは特に、関係のない状態変更が原因でコンポーネントが頻繁に再レンダリングされるシナリオで役立ちます。useMemoは場面を選んで使用してください。過度に使用すると、不要なメモリオーバーヘッドが追加される可能性があります。
依存関係が変更されるたびにメモ化された値が再計算されるようにするには、useMemoの依存配列に項目を追加する必要があります。依存関係を省略すると、古い値や不正確な値が生じる可能性があります。一方、不要な依存関係は不要な再計算を引き起こし、パフォーマンス上の利点が減少する可能性があります。
useStateとuseTransition
ReactのuseStateの使用を最小限に抑えると、不要な再レンダリングが減り、パフォーマンスが向上する可能性があります。再レンダリングは状態の更新のたびにトリガーされるため、過剰な状態管理はパフォーマンスのボトルネックになることがあります。これは特に、複雑なコンポーネントに当てはまります。レンダリングに影響しない可変値には参照(useRef)を使用するなどしてuseStateの使用を最小限に抑えたり、冗長な値を保存する代わりに状態を引き継いだりすると、コンポーネントをより効率化できます。useTransitionフックは、ユーザー入力などの緊急の状態更新に優先順位を付けることでパフォーマンスを高めます。同時に、検索結果などの重要度の低い状態更新は延期されます。これによりUIのブロックが防止され、コストの高い状態遷移中でも対話操作の応答性が維持されるため、よりスムーズなエクスペリエンスを実現できます。状態設定の呼び出しをuseTransitionからのstartTransition関数でラップすると、Reactは、古い状態更新のレンダリングを中断して最新の状態更新のレンダリングを優先できるようになります。
サンプルアプリ
最適化前
import React, { useState, useEffect } from 'react';
import {
View,
Text,
Button,
FlatList,
TouchableOpacity
} from 'react-native';
// 悪い例: メモ化されていないデータと関数
const BadPracticeApp = () => {
const [count, setCount] = useState(0);
const [selectedItem, setSelectedItem] = useState(null);
// レンダリングのたびにコストの高い計算が実行され、BadPracticeAppレベルの再レンダリング時には新しい関数インスタンスが生成されます。
const sumOfSquares = (num) => {
console.log('平方和を計算します...');
return Array.from(
{ length: num },
(_, i) => (i + 1) ** 2
).reduce((acc, val) => acc + val, 0);
};
// dataはBadPracticeAppの再レンダリングのたびに再作成されます。
const data = Array.from(
{ length: 50 },
(_, i) => `Item ${i + 1}`
);
return (
<View style={{ flex: 1, padding: 20 }}>
<Text>Sum of squares: {sumOfSquares(10)}</Text>
<Button
title="カウントを増やす"
onPress={() => setCount(count + 1)}
/>
<FlatList
data={data}
keyExtractor={(item) => item} // 匿名関数が使用されているため、再レンダリングのたびに新しい関数インスタンスが作成されます。
renderItem={({ item }) => (
<TouchableOpacity onPress={() => setSelectedItem(item)}>
<Text
style={{
padding: 10,
backgroundColor:
item === selectedItem ? 'lightblue' : 'white'
}}>
{item}
</Text>
</TouchableOpacity>
)} // 匿名関数が使用されているため、再レンダリングのたびに新しい関数インスタンスが作成され、返された関数コンポーネントはメモ化されません。
/>
</View>
);
};
export default BadPracticeApp;
最適化後
import React, {
memo,
StrictMode,
useState,
useMemo,
useCallback,
useTransition
} from 'react';
import {
View,
Text,
Button,
FlatList,
TouchableOpacity,
ActivityIndicator
} from 'react-native';
// 不要な再レンダリングを防ぐためにメモ化されたItemコンポーネント
const Item = memo(({ item, isSelected, onPress }) => {
return (
<TouchableOpacity onPress={() => onPress(item)}>
<Text
style=>
{item}
</Text>
</TouchableOpacity>
);
});
const BestPracticeApp = () => {
const [count, setCount] = useState(0);
const [selectedItem, setSelectedItem] = useState(null);
const [isPending, startTransition] = useTransition(); // 緊急でない更新の処理用
// コストの高い計算をメモ化します。
const sumOfSquares = useMemo(() => {
console.log('平方和を計算します...');
return Array.from(
{ length: 10 },
(_, i) => (i + 1) ** 2
).reduce((acc, val) => acc + val, 0);
}, []); // 依存関係が変更された場合にのみ再計算されます(この場合はなし)。
// dataをメモ化して配列の不要な再作成を防ぎます。
const data = useMemo(
() => Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`),
[]
);
// renderItem関数をメモ化して不要な再レンダリングを防ぎます。
const renderItem = useCallback(
({ item }) => (
<Item
item={item}
isSelected={item === selectedItem}
onPress={handleSelectItem}
/>
),
[selectedItem] // selectedItemの変更時にのみ再作成されます。
);
// アイテムが選択されたときの遅延処理(追加データの取得など)をシミュレートします。
const handleSelectItem = (item) => {
startTransition(() => {
setSelectedItem(item); // 選択されたアイテムをマークします。
});
};
return (
<StrictMode>
<View style={{ flex: 1, padding: 20 }}>
<Text>Sum of squares: {sumOfSquares}</Text>
<Button
title="カウントを増やす"
onPress={() => setCount(count + 1)}
/>
<Text>カウント:{count}</Text>
<FlatList
data={data}
keyExtractor={(item) => item}
renderItem={renderItem}
getItemLayout={(data, index) => ({
length: 50,
offset: 50 * index,
index
})}
/>
{isPending && (
<ActivityIndicator size="large" color="#0000ff" />
)}
</View>
</StrictMode>
);
};
export default BestPracticeApp;
大規模なリストの処理方法
React Nativeには、多数のさまざまなリストコンポーネントがあります。
ScrollView
パフォーマンスの観点では、通常、ScrollViewコンポーネントよりもFlatListコンポーネントを使用する方が良い結果が得られます。FlatListでは、仮想化によって子コンポーネントのレンダリングが遅延実行されるためです。ScrollViewコンポーネントでは、すべてのReact子コンポーネントが一度にレンダリングされます。このため、ScrollViewコンポーネントに含まれている子コンポーネントの数が多いほどパフォーマンスに大きく影響します。レンダリングは低速になり、メモリ使用量は増加します。ただし、データセットが小さいリストでは、ScrollViewを使用してもまったく問題はありません。ScrollViewとその子を適切にメモ化することを忘れないようにしてください。
FlatList
FlatListの必須プロパティであるdataとrenderItemを設定しただけでは、FlatListコンポーネントを使用するパフォーマンス上の利点がすべて有効になるわけではありません。アプリの滑らかさ(FPS)、CPU使用率、メモリ使用量を改善するには、FlatListコンポーネントのほかのプロパティを設定します。React Nativeの公式記事のOptimizing FlatList Configuration(英語のみ)で説明されているベストプラクティスをすべて適用してください。Amazonでのテストから、滑らかさとCPU使用率には、getItemLayout、windowSize、initialNumToRenderの各プロパティに加えて、子コンポーネントにメモ化を適用する(英語のみ)ことが重要であると判明しました。アプリで垂直スクロールと水平スクロールをサポートするためにFlatListを入れ子にする場合は、入れ子になったFlatListコンポーネントをメモ化する必要があります。
FlashList
Vegaでは、ShopifyのFlashListパッケージ(英語のみ)が完全にサポートされています。これはReact NativeのFlatListコンポーネントを置き換えることができ、より高いパフォーマンスを実現します。どちらも同じコンポーネントプロパティを使用しているため、FlatListからFlashListへの置き換えは簡単です。ただし、FlatListに固有の一部のプロパティは、FlashListでは機能しなくなります。このようなプロパティには、windowSize、getItemLayout, initialNumToRender、maxToRenderPerBatch、updateCellsBatchingPeriodなどがあります。完全な一覧は、使用方法に関する記事(英語のみ)の最後に記載されています。FlashListのパフォーマンスを向上させるには、いくつかの重要なプロパティを設定し、以下のベストプラクティスに従ってください。
- まず、
FlashListにestimatedItemSizeプロパティを必ず設定します。FlashListでestimatedItemSizeプロパティを使用すると、適切な数のアイテムの事前レンダリングが可能になり、空白の発生や読み込み時間が最小限に抑えられるため、パフォーマンスの向上につながります。また、不要な再レンダリングや大規模なレンダリングツリーが回避され、高速スクロール時の応答性も向上します。詳細については、Estimated Item Size Prop(英語のみ)を参照してください。 FlashListでアイテムのリサイクルを使用すると、画面外のコンポーネントが破棄されずに再利用され、不要な再レンダリングが防止され、メモリ使用量が削減されます。これによってパフォーマンスが向上します。最適な動作のためには、リサイクルされるコンポーネントの動的プロパティに対してuseStateを使用しないようにしてください。使用すると、以前のアイテムの状態の値が保持されて効率が悪くなります。詳細については、リサイクルに関する記事(英語のみ)を参照してください。- アイテムコンポーネントおよびその下に入れ子になったコンポーネントから、keyプロパティを削除します。詳細については、Remove key prop(英語のみ)を参照してください。
- 使用しているセルコンポーネントの型が複数あり、それらの違いが大きい場合は、
getItemTypeプロパティを利用することを検討してください。詳細については、getItemType(英語のみ)を参照してください。
Imageコンポーネント
iOSおよびAndroid上のReact NativeのImageコンポーネントには、キャッシュのようなすぐに使用できるパフォーマンス最適化機能は用意されていません。React Nativeアプリで使用される画像をメモリレベルまたはディスクレベルでキャッシュする場合、React Native開発者は通常、react-native-fast-image(英語のみ)やexpo-image(英語のみ)などのコミュニティパッケージを使用します。Vega向けReact Nativeでは、Imageコンポーネントのネイティブ実装全体にキャッシュメカニズムが組み込まれています。これはreact-native-fast-imageやexpo-imageのImageと同じように動作します。
ユースケースに合わせて、同じ画像アセットを複数のサイズと解像度で用意することをお勧めします。たとえば、FlatListコンポーネントやScrollViewコンポーネント内でImageコンポーネントをレンダリングする場合は、トリミングしたバージョンまたはサムネイルのサイズのアセット(英語のみ)を使用する方が適しています。これにより、画像のデコードに費やされるCPUサイクルが少なくなり、未加工の画像アセットに使用されるメモリ量も減少します。
Animatedライブラリ
React Nativeには、React Nativeのコアコンポーネントで滑らかなアニメーションを実現するAnimatedライブラリ(英語のみ)が用意されています。React Nativeでのアニメーションはコストの高い操作です。フレームごとに、JSスレッドはアニメーションの更新を計算し、ブリッジ経由でネイティブ側に送信してフレームを生成します。アニメーションを使用すると、同時に実行されているほかのプロセスも影響を受ける可能性があります。その結果、JSスレッドが過負荷になり、タイマーの処理、Reactフックの実行、コンポーネントの更新、その他の処理ができなくなることがあります。これを回避するには、アニメーションのuseNativeDriverプロパティをtrueに設定します。useNativeDriverは、ネイティブアニメーションではtrueに設定され、JSアニメーションではfalseに設定されます。詳細については、Using the native driver(英語のみ)を参照してください。
また、InteractionManager(英語のみ)を使用してアニメーションをスケジュールすることもお勧めします。InteractionManager.runAfterInteractions()は、コールバック関数またはPromiseTaskオブジェクトを受け取り、現在のアニメーションやその他の対話操作が完了した後にのみ実行されるようにします。新しくスケジュールされたアニメーションの実行を遅らせると、JSスレッドが過負荷になる可能性が減り、アプリ全体の応答性と滑らかさが向上します。
フォーカス管理UIの最適化
コンポーネントがフォーカスを受け取ると、そのonFocusコールバックが呼び出されます。コンポーネントがフォーカスを失うと、そのonBlurコールバックが呼び出されます。これが、ユーザーがアプリ内を移動したときにUIの変更を行うための主な手段となります。onFocusとonBlurのハンドラー関数はできるだけシンプルにしてください。これを達成するにはいくつかの方法があります。
フォーカスのあるアイテムの周囲に境界線を描画するだけのコンポーネントでは、条件付きスタイルを使用します。たとえば、styles={isFocused ? styles.focusedStyle : styles.blurredStyle}のように指定できます。onFocusとonBlurの呼び出しの組み合わせごとに発生するReactレンダリングサイクルは、最大でも1回に抑えるようにします。これにより、JSスレッドで実行される作業量が最小限になり、ユーザーナビゲーションの応答性が維持されます。
コンポーネントがフォーカスを受け取ったときに、コンポーネントでより複雑なUIの更新を行う必要がある場合は、その作業をネイティブアニメーションで実行して、JSスレッド自体では行われないようにします。このようなUI更新の例として、選択されたビュー、テキスト、画像の拡大や、不透明度の変更があります。さらに最適化するには、デバウンスメカニズムを追加して、ユーザーが一定時間アイテムにフォーカスを置いた状態にした場合にのみこのネイティブアニメーションを開始します。アニメーションの開始が少し遅くなるというわずかなペナルティが発生しますが、この遅延により、ユーザーがアプリで高速スクロールやナビゲーションを行ったときに、フォーカスを受け取ったすべてのアイテムでアニメーションが開始されるのを防ぐことができます。
リスナー、イベントサブスクリプション、タイマー
KeplerAppState、DeviceInfo、Keyboardなどの一部のターボモジュール(TM)では、特定のイベントにリスナーを登録できます。これらのリスナーは、useEffectフック内で作成されることがよくあります。無効なメモリが発生するのを防ぐために、useEffectフックのreturn関数内でリスナーをクリーンアップしてください。
useEffect(() => {
const keboardShowListenerHandler = Keyboard.addListener(
'keyboardDidShow',
handleKeyboardDidShow
);
return () => {
keboardShowListenerHandler.remove();
};
}, []);
オーバードローの削減
Vega向けReact Nativeアプリはキャンバスのようなものです。同じスペースに背景色の異なるビューが入れ子に配置されている場合は、同じ領域を何度も塗り重ねることになります。たとえば、全画面の黒いビューが全画面のグレーのビューで覆われていると、黒いレイヤーは完全に「オーバードロー」されます。 これによって目には見えなくなりますが、処理は引き続き行われます。オーバードローによっては許容できるものもあり、特に部分的なオーバードローは許容されますが、このような冗長な操作を最小限に抑えるとパフォーマンスが向上します。目標は、フレームごとの各ピクセルの描画回数をできるだけ少なくすることです。起動クエリ引数?SHOW_OVERDRAWN=trueを付けてアプリを実行すると、アプリ内で発生するオーバードローを検出できます。
vda shell vlcm launch-app "pkg://com.amazon.keplersampleapp.main?SHOW_OVERDRAWN=true"
これにより、キャンバス上のUI要素に半透明の色が重ねて表示されます。結果の色は、キャンバスがオーバードローされた回数を示します。
- 実際の色: オーバードローなし
- 青: オーバードローが1回発生。
- 緑: オーバードローが2回発生。
- ピンク: オーバードローが3回発生。
- 赤: オーバードローが4回以上発生。
アプリのどの部分もピンクや赤で表示されないことが理想です。つまり、3つ以上のレイヤーでオーバードローされることがないようにします。
バンドルサイズの削減
どのようなReactアプリでも、メモリ使用量を最適化し、アプリの起動時間を短縮するには、バンドルサイズを最小限に抑えることが重要です。react-native-bundle-visualizer(英語のみ)は、コストの高いimportステートメントや使用されない依存関係を特定するために役立ちます。インストール後に次のコマンドを実行すると、ブラウザで自動的にHTMLファイルが開き、インタラクティブに確認できます。
npx react-native-bundle-visualizer
以下に示すサンプルのVega向けReact Nativeアプリバンドルでは、import { debounce } from 'lodash';というステートメントにより、lodashからdebounceを使用しようとしています。このimportステートメントの後、バンドルは次のように視覚化されます。
上の画像から、lodashは493.96KB(17.8%)であることがわかります。このimportステートメントをimport debounce from 'lodash/debounce';に切り替えると、バンドルサイズは大幅に小さくなります。
上の画像では、lodashはわずか13.19KB(0.6%)になっています。このimportステートメントの変更により、アプリのバンドルサイズが480.77KB削減されました。
アプリの初回起動時間の短縮
スプラッシュ画面を効率的に表示するには、VegaのネイティブのSplashScreen APIを使用します。このAPIでは、vpkgにバンドルされた未加工の画像アセットからスプラッシュ画面をネイティブにレンダリングするため、アプリの起動時にすぐにスプラッシュ画面が表示されます。このアプローチにより、JavaScriptベースのスプラッシュ画面をレンダリングする代わりに、コンテンツの読み込みやネットワーク呼び出しなど、重要なタスクを処理するためにアプリのJavaScriptスレッドを使用できるようになります。ネイティブのスプラッシュ画面は、アプリの最初のフレームがレンダリングされた後に自動的に閉じられます。スプラッシュ画面を閉じるタイミングをカスタマイズするには、2つのメソッドを使用します。usePreventHideSplashScreen()を呼び出して、スプラッシュ画面の自動消去をオーバーライドします。さらにuseHideSplashScreenCallback()を呼び出して、スプラッシュ画面を非表示にします。
関連トピック
- アプリのKPIを測定
- Vega ESLintプラグインを使用してパフォーマンスの問題を検出する方法
- WebViewアプリを開発する場合は、Vegaウェブアプリパフォーマンスに関するベストプラクティスを参照してください。
Last updated: 2025年10月1日



