JavaScriptの読み込みの最適化
JavaScriptコードの解析と実行にはメモリと時間が必要です。そのため、アプリが成長するにつれて、コードを初めて必要になるまで読み込みを遅延させると便利なことがよくあります。React Nativeにはデフォルトで有効になっている標準的な最適化がいくつかあり、Reactがアプリをより効率的にロードするのに役立つテクニックを独自のコードに採用できます。また、非常に大きなアプリに適した高度な自動最適化(独自のトレードオフあり)もいくつかあります。
推奨:Hermesを使用する
Hermesは、新しいReact Nativeアプリのデフォルトエンジンであり、効率的なコード読み込みのために高度に最適化されています。リリースビルドでは、JavaScriptコードは事前にバイトコードに完全にコンパイルされます。バイトコードは必要に応じてオンデマンドでメモリにロードされ、プレーンなJavaScriptのように解析する必要はありません。
React NativeでHermesを使用する方法について詳しくは、こちらをご覧ください。
推奨:大きなコンポーネントを遅延ロードする
多くのコード/依存関係を持つコンポーネントがアプリの初期レンダリング時に使用される可能性が低い場合は、Reactのlazy
APIを使用して、最初にレンダリングされるまでコードの読み込みを延期できます。通常、アプリの画面レベルのコンポーネントを遅延ロードすることを検討する必要があります。これにより、アプリに新しい画面を追加しても起動時間が長くなることはありません。
コード例を含む、Suspenseを使用したコンポーネントの遅延ロードについては、Reactのドキュメントをご覧ください。
ヒント:モジュールの副作用を避ける
コンポーネントモジュール(またはその依存関係)に、グローバル変数の変更やコンポーネント外のイベントへのサブスクライブなど、副作用がある場合、コンポーネントの遅延ロードはアプリの動作を変更する可能性があります。Reactアプリのほとんどのモジュールには副作用があってはなりません。
import Logger from './utils/Logger';
// 🚩 🚩 🚩 Side effect! This must be executed before React can even begin to
// render the SplashScreen component, and can unexpectedly break code elsewhere
// in your app if you later decide to lazy-load SplashScreen.
global.logger = new Logger();
export function SplashScreen() {
// ...
}
高度:require
をインラインで呼び出す
lazy
や非同期import()
を使用せずに、初めて使用するまでコードの読み込みを延期したい場合があります。これを行うには、ファイルの先頭で静的なimport
を使用する代わりに、require()
関数を使用します。
import {Component} from 'react';
import {Text} from 'react-native';
// ... import some very expensive modules
export default function VeryExpensive() {
// ... lots and lots of rendering logic
return <Text>Very Expensive Component</Text>;
}
import {useCallback, useState} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
// Usually we would write a static import:
// import VeryExpensive from './VeryExpensive';
let VeryExpensive = null;
export default function Optimize() {
const [needsExpensive, setNeedsExpensive] = useState(false);
const didPress = useCallback(() => {
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}
setNeedsExpensive(true);
}, []);
return (
<View style={{marginTop: 20}}>
<TouchableOpacity onPress={didPress}>
<Text>Load</Text>
</TouchableOpacity>
{needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
高度:require
呼び出しを自動的にインライン化する
React Native CLIを使用してアプリをビルドする場合、require
呼び出し(import
ではない)は、コードと使用するサードパーティパッケージ(node_modules
)の両方で自動的にインライン化されます。
import {useCallback, useState} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
// This top-level require call will be evaluated lazily as part of the component below.
const VeryExpensive = require('./VeryExpensive').default;
export default function Optimize() {
const [needsExpensive, setNeedsExpensive] = useState(false);
const didPress = useCallback(() => {
setNeedsExpensive(true);
}, []);
return (
<View style={{marginTop: 20}}>
<TouchableOpacity onPress={didPress}>
<Text>Load</Text>
</TouchableOpacity>
{needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
一部のReact Nativeフレームワークでは、この動作が無効になっています。特に、Expoプロジェクトでは、require
呼び出しはデフォルトではインライン化されません。この最適化を有効にするには、プロジェクトのMetro設定を編集し、getTransformOptions
でinlineRequires: true
を設定します。
インラインrequire
の落とし穴
require
呼び出しをインライン化すると、モジュールが評価される順序が変わり、モジュールが決して評価されないことさえあります。JavaScriptモジュールは副作用がないように記述されることが多いため、これは通常、自動的に行っても安全です。
モジュールの1つに副作用がある場合(たとえば、何らかのログメカニズムを初期化したり、コードの残りの部分で使用されるグローバルAPIをパッチしたりする場合)、予期しない動作やクラッシュが発生する可能性があります。そのような場合は、特定のモジュールをこの最適化から除外するか、完全に無効にすることをお勧めします。
require
呼び出しのすべての自動インライン化を無効にするには:
metro.config.js
を更新して、inlineRequires
トランスフォーマーオプションをfalse
に設定します。
module.exports = {
transformer: {
async getTransformOptions() {
return {
transform: {
inlineRequires: false,
},
};
},
},
};
特定のモジュールのみをrequire
のインライン化から除外するには:
関連するトランスフォーマーオプションは2つあります。inlineRequires.blockList
とnonInlinedRequires
です。それぞれを使用する方法の例については、コードスニペットを参照してください。
module.exports = {
transformer: {
async getTransformOptions() {
return {
transform: {
inlineRequires: {
blockList: {
// require() calls in `DoNotInlineHere.js` will not be inlined.
[require.resolve('./src/DoNotInlineHere.js')]: true,
// require() calls anywhere else will be inlined, unless they
// match any entry nonInlinedRequires (see below).
},
},
nonInlinedRequires: [
// require('react') calls will not be inlined anywhere
'react',
],
},
};
},
},
};
インラインrequire
の設定と微調整の詳細については、MetroのgetTransformOptions
のドキュメントを参照してください。
高度:ランダムアクセスのモジュールバンドルを使用する(Hermes以外)
Hermesを使用している場合はサポートされていません。 HermesのバイトコードはRAMバンドル形式と互換性がなく、すべてのユースケースで同じ(またはそれ以上)のパフォーマンスを提供します。
ランダムアクセスのモジュールバンドル(RAMバンドルとも呼ばれます)は、上記のテクニックと連携して、解析してメモリにロードする必要があるJavaScriptコードの量を制限します。各モジュールは、モジュールを実行する必要がある場合にのみ解析される個別の文字列(またはファイル)として格納されます。
RAMバンドルは、物理的に別々のファイルに分割される場合もあれば、単一のファイル内の複数のモジュールのルックアップテーブルで構成される、インデックス付き形式を使用する場合もあります。
- Android
- iOS
AndroidでRAM形式を有効にするには、android/app/build.gradle
ファイルを編集します。apply from: "../../node_modules/react-native/react.gradle"
の行の前に、project.ext.react
ブロックを追加または修正します。
project.ext.react = [
bundleCommand: "ram-bundle",
]
単一のインデックス付きファイルを使用する場合は、Androidで次の行を使用します
project.ext.react = [
bundleCommand: "ram-bundle",
extraPackagerArgs: ["--indexed-ram-bundle"]
]
iOSでは、RAMバンドルは常にインデックス付き(=単一ファイル)です。
XcodeでRAM形式を有効にするには、ビルドフェーズ「Bundle React Native code and images」を編集します。../node_modules/react-native/scripts/react-native-xcode.sh
の前にexport BUNDLE_COMMAND="ram-bundle"
を追加します。
export BUNDLE_COMMAND="ram-bundle"
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh
RAMバンドルの設定と微調整の詳細については、MetroのgetTransformOptions
のドキュメントを参照してください。