跳至主要内容

RAM 包和内联 require

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

加载 JavaScript

在 react-native 可以执行 JS 代码之前,必须将该代码加载到内存中并解析。使用标准捆绑包,如果您加载一个 50mb 的捆绑包,则必须加载和解析所有 50mb,然后才能执行任何部分。RAM 捆绑包背后的优化是,您只需加载启动时实际需要的 50mb 的一部分,并在需要时逐步加载更多捆绑包。

内联 require

内联需要延迟模块或文件的加载,直到该文件真正需要时才加载。一个基本的例子如下所示

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 格式,内联需要也可以提高启动时间,因为 VeryExpensive.js 中的代码只会在第一次需要时执行。

启用 RAM 格式

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

在 Xcode 中,通过编辑构建阶段“捆绑 React Native 代码和图像”来启用 RAM 格式。在 ../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

在 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"]
]
info

如果你正在使用 Hermes JS 引擎,你不应该启用 RAM 捆绑功能。在 Hermes 中,加载字节码时,mmap 确保不会加载整个文件。使用 Hermes 和 RAM 捆绑可能会导致问题,因为这些机制彼此不兼容。

配置预加载和内联需要

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

调查已加载的模块

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

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 并查看是否有任何意外情况。请注意,内联 require 在第一次引用导入时被调用。您可能需要调查和重构以确保仅在启动时加载所需的模块。请注意,您可以更改 require 上的 Systrace 对象以帮助调试有问题的 require

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

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

更新 metro.config.js

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

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.

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

测试和衡量改进

您现在应该可以使用 RAM 格式和内联 require 构建您的应用程序。请确保您测量了启动前后的时间。