Skip to main content

RAM 打包包和内联要求

如果你有一个大型应用,你可能需要考虑随机访问模块 (RAM) 打包格式,并使用内联要求。这对于具有大量屏幕的应用非常有用,而这些屏幕在应用的典型使用过程中可能永远不会打开。通常,它对于具有大量启动后一段时间内不需要的代码的应用很有用。例如,该应用包含复杂的个人资料屏幕或较少使用的功能,但大多数会话仅涉及访问应用的主屏幕以获取更新。我们可以通过使用 RAM 格式并要求这些功能和屏幕内联(当它们实际使用时)来优化打包包的加载。

¥If you have a large app you may want to consider the Random Access Modules (RAM) bundle format, and using inline requires. This is useful for apps that have a large number of screens which may not ever be opened during a typical usage of the app. Generally it is useful to apps that have large amounts of code that are not needed for a while after startup. For instance the app includes complicated profile screens or lesser used features, but most sessions only involve visiting the main screen of the app for updates. We can optimize the loading of the bundle by using the RAM format and requiring those features and screens inline (when they are actually used).

加载 JavaScript

¥Loading JavaScript

在 react-native 执行 JS 代码之前,必须将该代码加载到内存中并进行解析。对于标准打包包,如果加载 50mb 的打包包,则必须先加载并解析所有 50mb 的打包包,然后才能执行其中任何一个打包包。RAM 打包包背后的优化是,你可以仅加载启动时实际需要的 50mb 部分,并根据需要逐步加载更多打包包。

¥Before react-native can execute JS code, that code must be loaded into memory and parsed. With a standard bundle if you load a 50mb bundle, all 50mb must be loaded and parsed before any of it can be executed. The optimization behind RAM bundles is that you can load only the portion of the 50mb that you actually need at startup, and progressively load more of the bundle as those sections are needed.

内联要求

¥Inline Requires

内联需要延迟对模块或文件的需求,直到实际需要该文件为止。一个基本的例子如下所示:

¥Inline requires delay the requiring of a module or file until that file is actually needed. A basic example would look like this:

VeryExpensive.tsx
import React, {Component} from 'react';
import {Text} from 'react-native';
// ... import some very expensive modules

// You may want to log at the file level to verify when this is happening
console.log('VeryExpensive component loaded');

export default class VeryExpensive extends Component {
// lots and lots of code
render() {
return <Text>Very Expensive Component</Text>;
}
}
Optimized.tsx
import React, {Component} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';

let VeryExpensive = null;

export default class Optimized extends Component {
state = {needsExpensive: false};

didPress = () => {
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}

this.setState(() => ({
needsExpensive: true,
}));
};

render() {
return (
<View style={{marginTop: 20}}>
<TouchableOpacity onPress={this.didPress}>
<Text>Load</Text>
</TouchableOpacity>
{this.state.needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
}

即使没有 RAM 格式,内联 require 也可以缩短启动时间,因为 VeryExpective.js 中的代码只会在第一次需要时执行。

¥Even without the RAM format, inline requires can lead to startup time improvements, because the code within VeryExpensive.js will only execute once it is required for the first time.

启用 RAM 格式

¥Enable the RAM format

在 iOS 上,使用 RAM 格式将创建一个索引文件,该文件将一次加载一个模块。在 Android 上,默认情况下它会为每个模块创建一组文件。你可以强制 Android 创建单个文件,如 iOS,但使用多个文件可以提高性能并且需要更少的内存。

¥On iOS using the RAM format will create a single indexed file that react native will load one module at a time. On Android, by default it will create a set of files for each module. You can force Android to create a single file, like iOS, but using multiple files can be more performant and requires less memory.

通过编辑构建阶段 "打包 React Native 代码和图片". 在 Xcode 中启用 RAM 格式。在 ../node_modules/react-native/scripts/react-native-xcode.sh 之前添加 export BUNDLE_COMMAND="ram-bundle"

¥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

在 Android 上,通过编辑 android/app/build.gradle 文件启用 RAM 格式。在 apply from: "../../node_modules/react-native/react.gradle" 行之前添加或修改 project.ext.react 块:

¥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",
]

如果你想使用单个索引文件,请在 Android 上使用以下行:

¥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"]
]
信息

如果你使用的是 Hermes JS 引擎,则不应启用 RAM 打包功能。在 Hermes 中,加载字节码时,mmap 确保不加载整个文件。将 Hermes 与 RAM 打包一起使用可能会导致问题,因为这些机制彼此不兼容。

¥If you are using Hermes JS Engine, you should not have RAM bundles feature enabled. In Hermes, when loading the bytecode, mmap ensures that the entire file is not loaded. Using Hermes with RAM bundles might lead to issues, because those mechanisms are not compatible with each other.

配置预加载和内联需求

¥Configure Preloading and Inline Requires

现在我们有了一个 RAM 包,调用 require 就会产生开销。当 require 遇到尚未加载的模块时,现在需要通过桥发送消息。这对启动影响最大,因为在应用加载初始模块时,可能会发生最多数量的 require 调用。幸运的是,我们可以配置部分模块进行预加载。为此,你需要实现某种形式的内联 require。

¥Now that we have a RAM bundle, there is overhead for calling require. require now needs to send a message over the bridge when it encounters a module it has not loaded yet. This will impact startup the most, because that is where the largest number of require calls are likely to take place while the app loads the initial module. Luckily we can configure a portion of the modules to be preloaded. In order to do this, you will need to implement some form of inline require.

研究加载的模块

¥Investigating the Loaded Modules

在根文件 (index.(ios|android).js) 中,你可以在初始导入后添加以下内容:

¥In your root file (index.(ios|android).js) you can add the following after the initial imports:

const modules = require.getModules();
const moduleIds = Object.keys(modules);
const loadedModuleNames = moduleIds
.filter(moduleId => modules[moduleId].isInitialized)
.map(moduleId => modules[moduleId].verboseName);
const waitingModuleNames = moduleIds
.filter(moduleId => !modules[moduleId].isInitialized)
.map(moduleId => modules[moduleId].verboseName);

// make sure that the modules you expect to be waiting are actually waiting
console.log(
'loaded:',
loadedModuleNames.length,
'waiting:',
waitingModuleNames.length,
);

// grab this text blob, and put it in a file named packager/modulePaths.js
console.log(
`module.exports = ${JSON.stringify(
loadedModuleNames.sort(),
null,
2,
)};`,
);

当你运行应用时,你可以在控制台中查看已加载的模块数量以及正在等待的模块数量。你可能想阅读 moduleNames 并看看是否有任何惊喜。请注意,第一次引用导入时会调用内联需求。你可能需要进行调查和重构,以确保在启动时仅加载你想要的模块。请注意,你可以根据需要更改 Systrace 对象,以帮助调试有问题的需求。

¥When you run your app, you can look in the console and see how many modules have been loaded, and how many are waiting. You may want to read the moduleNames and see if there are any surprises. Note that inline requires are invoked the first time the imports are referenced. You may need to investigate and refactor to ensure only the modules you want are loaded on startup. Note that you can change the Systrace object on require to help debug problematic requires.

require.Systrace.beginEvent = message => {
if (message.includes(problematicModule)) {
throw new Error();
}
};

每个应用都是不同的,但仅加载第一个屏幕所需的模块可能是有意义的。当你满意后,将 loadedModuleNames 的输出放入名为 packager/modulePaths.js.txt 的文件中。

¥Every app is different, but it may make sense to only load the modules you need for the very first screen. When you are satisfied, put the output of the loadedModuleNames into a file named packager/modulePaths.js.

更新 Metro.config.js

¥Updating the metro.config.js

我们现在需要更新项目根目录中的 metro.config.js 以使用新生成的 modulePaths.js 文件:

¥We now need to update metro.config.js in the root of the project to use our newly generated modulePaths.js file:

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const path = require('path');
const modulePaths = require('./packager/modulePaths');

const config = {
transformer: {
getTransformOptions: () => {
const moduleMap = {};
modulePaths.forEach(modulePath => {
if (fs.existsSync(modulePath)) {
moduleMap[path.resolve(modulePath)] = true;
}
});
return {
preloadedModules: moduleMap,
transform: {inlineRequires: {blockList: moduleMap}},
};
},
},
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

另见 配置 Metro

¥See also Configuring Metro.

配置中的 preloadedModules 条目指示在构建 RAM 打包包时应将哪些模块标记为预加载。加载打包包时,在执行任何需求之前,会立即加载这些模块。blockList 条目表示不应要求内联这些模块。由于它们是预加载的,因此使用内联需求不会带来性能优势。事实上,每次引用导入时,生成的 JavaScript 都会花费额外的时间来解析内联 require。

¥The preloadedModules entry in the config indicates which modules should be marked as preloaded when building a RAM bundle. When the bundle is loaded, those modules are immediately loaded, before any requires have even executed. The blockList entry indicates that those modules should not be required inline. Because they are preloaded, there is no performance benefit from using an inline require. In fact the generated JavaScript spends extra time resolving the inline require every time the imports are referenced.

测试和测量改进

¥Test and Measure Improvements

你现在应该准备好使用 RAM 格式和内联要求构建你的应用。确保测量启动前后的时间。

¥You should now be ready to build your app using the RAM format and inline requires. Make sure you measure the before and after startup times.