Skip to main content

Optimizing JavaScript loading

Parsing and running JavaScript code requires memory and time. Because of this, as your app grows, it's often useful to delay loading code until it's needed for the first time. React Native comes with some standard optimizations that are on by default, and there are techniques you can adopt in your own code to help React load your app more efficiently. There are also some advanced automatic optimizations (with their own tradeoffs) that are suitable for very large apps.

Hermes is the default engine for new React Native apps, and is highly optimized for efficient code loading. In release builds, JavaScript code is fully compiled to bytecode ahead of time. Bytecode is loaded to memory on-demand and does not need to be parsed like plain JavaScript does.

info

Read more about using Hermes in React Native here.

If a component with a lot of code/dependencies is not likely to be used when initially rendering your app, you can use React's lazy API to defer loading its code until it's rendered for the first time. Typically, you should consider lazy-loading screen-level components in your app, so that adding new screens to your app does not increase its startup time.

info

Read more about lazy-loading components with Suspense , including code examples, in React's documentation.

Tip: Avoid module side effects​

Lazy-loading components can change the behavior of your app if your component modules (or their dependencies) have side effects, such as modifying global variables or subscribing to events outside of a component. Most modules in React apps should not have any side effects.

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() {
// ...
}

Advanced: Call require inline​

Sometimes you may want to defer loading some code until you use it for the first time, without using lazy or an asynchronous import(). You can do this by using the require() function where you would otherwise use a static import at the top of the file.

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

Advanced: Automatically inline require calls​

If you use the React Native CLI to build your app, require calls (but not imports) will automatically be inlined for you, both in your code and inside any third-party packages (node_modules) you use.

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

Some React Native frameworks disable this behavior. In particular, in Expo projects, require calls are not inlined by default. You can enable this optimization by editing your project's Metro config and setting inlineRequires: true in getTransformOptions.

Pitfalls of inline requires​

Inlining require calls changes the order in which modules are evaluated, and can even cause some modules to never be evaluated. This is usually safe to do automatically, because JavaScript modules are often written to be side-effect-free.

If one of your modules does have side effects - for example, if it initializes some logging mechanism, or patches a global API used by the rest of your code - then you might see unexpected behavior or even crashes. In those cases, you may want to exclude certain modules from this optimization, or disable it entirely.

To disable all automatic inlining of require calls:

Update your metro.config.js to set the inlineRequires transformer option to false:

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

To only exclude certain modules from require inlining:

There are two relevant transformer options: inlineRequires.blockList and nonInlinedRequires. See the code snippet for examples of how to use each one.

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',
],
},
};
},
},
};

See the documentation for getTransformOptions in Metro for more details on setting up and fine-tuning your inline requires.

Advanced: Use random access module bundles (non-Hermes)​

info

Not supported when using Hermes. Hermes bytecode is not compatible with the RAM bundle format, and provides the same (or better) performance in all use cases.

Random access module bundles (also known as RAM bundles) work in conjunction with the techniques mentioned above to limit the amount of JavaScript code that needs to be parsed and loaded into memory. Each module is stored as a separate string (or file) which is only parsed when the module needs to be executed.

RAM bundles may be physically split into separate files, or they may use the indexed format, consisting of a lookup table of multiple modules in a single file.

On Android enable the RAM format by editing your android/app/build.gradle file. Before the line apply from: "../../node_modules/react-native/react.gradle" add or amend the project.ext.react block:

project.ext.react = [
bundleCommand: "ram-bundle",
]

Use the following lines on Android if you want to use a single indexed file:

project.ext.react = [
bundleCommand: "ram-bundle",
extraPackagerArgs: ["--indexed-ram-bundle"]
]

See the documentation for getTransformOptions in Metro for more details on setting up and fine-tuning your RAM bundle build.