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

JavaScriptの読み込みを最適化する

JavaScriptコードのパースと実行には、メモリと時間が必要です。このため、アプリが大きくなるにつれて、初めて必要になるまでコードの読み込みを遅延させることが有効な場合が多くなります。React Nativeには、デフォルトで有効になっている標準的な最適化がいくつか備わっており、Reactがアプリをより効率的に読み込むのを助けるために、独自のコードで採用できるテクニックもあります。また、非常に大規模なアプリに適した、高度な自動最適化(トレードオフを伴います)も存在します。

Hermesは新しいReact Nativeアプリのデフォルトエンジンであり、効率的なコード読み込みのために高度に最適化されています。リリースビルドでは、JavaScriptコードは事前に完全にバイトコードにコンパイルされます。バイトコードはオンデマンドでメモリに読み込まれ、プレーンなJavaScriptのようにパースする必要がありません。

info

React NativeでのHermesの使用に関する詳細はこちらをご覧ください。

多くのコードや依存関係を持つコンポーネントが、アプリの初期レンダリング時に使用される可能性が低い場合、Reactのlazy APIを使用して、初めてレンダリングされるまでそのコードの読み込みを遅延させることができます。一般的に、アプリに新しい画面を追加しても起動時間が増加しないように、アプリのスクリーンレベルのコンポーネントを遅延読み込みすることを検討すべきです。

info

Reactのドキュメントで、コード例を含むSuspenseを使用したコンポーネントの遅延読み込みについて、さらに詳しく読むことができます。

ヒント: モジュールの副作用を避ける

コンポーネントモジュール(またはその依存関係)がグローバル変数の変更やコンポーネント外のイベントへの購読など、副作用を持つ場合、コンポーネントの遅延読み込みはアプリの動作を変える可能性があります。Reactアプリのほとんどのモジュールは副作用を持つべきではありません。

SideEffects.tsx
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()関数を使用することで実現できます。

VeryExpensive.tsx
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>;
}
Optimized.tsx
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)内の両方で自動的にインライン化されます。

tsx
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>
);
}
info

一部のReact Nativeフレームワークでは、この動作が無効になっています。特に、Expoプロジェクトでは、require呼び出しはデフォルトでインライン化されません。この最適化を有効にするには、プロジェクトのMetro設定を編集し、getTransformOptionsinlineRequires: trueを設定します。

インラインrequireの落とし穴

require呼び出しをインライン化すると、モジュールが評価される順序が変わり、一部のモジュールが全く評価されなくなることさえあります。JavaScriptモジュールは副作用がないように書かれていることが多いため、これは通常、自動的に行っても安全です。

もしあなたのモジュールの一つが副作用を持つ場合(例えば、ロギングメカニズムを初期化したり、コードの他の部分で使用されるグローバルAPIにパッチを当てたりする場合)、予期しない動作やクラッシュが発生する可能性があります。そのような場合は、特定のモジュールをこの最適化から除外したり、完全に無効にしたりする必要があるかもしれません。

require呼び出しのすべての自動インライン化を無効にするには:

metro.config.jsを更新して、inlineRequiresトランスフォーマオプションをfalseに設定します

metro.config.js
module.exports = {
transformer: {
async getTransformOptions() {
return {
transform: {
inlineRequires: false,
},
};
},
},
};

特定のモジュールのみをrequireインライン化から除外するには:

関連するトランスフォーマオプションは2つあります: inlineRequires.blockListnonInlinedRequiresです。それぞれの使用方法の例については、コードスニペットを参照してください。

metro.config.js
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)

info

Hermesを使用している場合はサポートされていません。HermesのバイトコードはRAMバンドル形式と互換性がなく、すべてのユースケースで同等(またはそれ以上)のパフォーマンスを提供します。

ランダムアクセスモジュールバンドル(RAMバンドルとも呼ばれます)は、上記で述べたテクニックと連携して、パースしてメモリにロードする必要があるJavaScriptコードの量を制限します。各モジュールは別々の文字列(またはファイル)として保存され、モジュールが実行される必要があるときにのみパースされます。

RAMバンドルは物理的に別々のファイルに分割されることもあれば、単一ファイル内に複数のモジュールのルックアップテーブルで構成されるインデックス形式を使用することもあります。

Androidでは、android/app/build.gradleファイルを編集してRAMフォーマットを有効にします。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"]
]

RAMバンドルビルドの設定と微調整の詳細については、MetroのgetTransformOptionsのドキュメントを参照してください。