RAM 包和内联 require
如果您有一个大型应用程序,您可能需要考虑使用随机访问模块 (RAM) 包格式和内联 require。这对于具有大量屏幕的应用程序很有用,这些屏幕在应用程序的典型使用过程中可能永远不会打开。通常,它对包含大量在启动后一段时间内不需要的代码的应用程序很有用。例如,应用程序包含复杂的配置文件屏幕或很少使用的功能,但大多数会话只涉及访问应用程序的主屏幕以获取更新。我们可以使用 RAM 格式和内联 require 这些功能和屏幕(当它们实际使用时)来优化捆绑包的加载。
加载 JavaScript
在 react-native 可以执行 JS 代码之前,必须将该代码加载到内存中并解析。使用标准捆绑包,如果您加载一个 50mb 的捆绑包,则必须加载和解析所有 50mb,然后才能执行任何部分。RAM 捆绑包背后的优化是,您只需加载启动时实际需要的 50mb 的一部分,并在需要时逐步加载更多捆绑包。
内联 require
内联需要延迟模块或文件的加载,直到该文件真正需要时才加载。一个基本的例子如下所示
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>;
}
}
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"]
]
如果你正在使用 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 构建您的应用程序。请确保您测量了启动前后的时间。