-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate inline requires + RAM bundles docs to new "optimizing JS load…
…ing" guide (#4030) * Rename ram-bundles-inline-requires.md -> optimizing-javascript-code-loading.md * First pass of new JS loading optimization guide * Add redirect from /docs/next/ram-bundles-inline-requires * Move side effects section into `lazy` section and reword * Drop the "code", it's cleaner * Apply review suggestions
- Loading branch information
Showing
4 changed files
with
225 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
--- | ||
id: optimizing-javascript-loading | ||
title: Optimizing JavaScript loading | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import constants from '@site/core/TabsConstants'; | ||
|
||
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. | ||
|
||
## Recommended: Use Hermes | ||
|
||
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](./hermes). | ||
::: | ||
|
||
## Recommended: Lazy-load large components | ||
|
||
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`](https://react.dev/reference/react/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 | ||
](https://react.dev/reference/react/lazy#suspense-for-code-splitting), 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. | ||
|
||
```tsx title="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()`](https://metrobundler.dev/docs/module-api/#require) function where you would otherwise use a static `import` at the top of the file. | ||
|
||
```tsx title="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>; | ||
} | ||
``` | ||
|
||
```tsx title="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={this.didPress}> | ||
<Text>Load</Text> | ||
</TouchableOpacity> | ||
{this.state.needsExpensive ? <VeryExpensive /> : null} | ||
</View> | ||
); | ||
} | ||
``` | ||
|
||
## Advanced: Automatically inline `require` calls | ||
|
||
If you use the React Native CLI to build your app, `require` calls (but not `import`s) will automatically be inlined for you, both in your code and inside any third-party packages (`node_modules`) you use. | ||
|
||
```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={this.didPress}> | ||
<Text>Load</Text> | ||
</TouchableOpacity> | ||
{this.state.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`](https://metrobundler.dev/docs/configuration#gettransformoptions). | ||
::: | ||
|
||
### Pitfalls of inline `require`s | ||
|
||
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`: | ||
|
||
```tsx title="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. | ||
|
||
```tsx title="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](https://metrobundler.dev/docs/configuration#gettransformoptions) for more details on setting up and fine-tuning your inline `require`s. | ||
|
||
## Advanced: Use random access module bundles (non-Hermes) | ||
|
||
:::info | ||
**Not supported when [using Hermes](#use-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. | ||
|
||
<Tabs groupId="platform" queryString defaultValue={constants.defaultPlatform} values={constants.platforms}> | ||
<TabItem value="android"> | ||
|
||
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"] | ||
] | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="ios"> | ||
|
||
On iOS, RAM bundles are always indexed ( = single file). | ||
|
||
Enable the RAM format in Xcode by editing the build phase "Bundle React Native code and images". Before `../node_modules/react-native/scripts/react-native-xcode.sh` add `export BUNDLE_COMMAND="ram-bundle"`: | ||
|
||
``` | ||
export BUNDLE_COMMAND="ram-bundle" | ||
export NODE_BINARY=node | ||
../node_modules/react-native/scripts/react-native-xcode.sh | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
See the documentation for [`getTransformOptions` in Metro](https://metrobundler.dev/docs/configuration#gettransformoptions) for more details on setting up and fine-tuning your RAM bundle build. |
Oops, something went wrong.