Implementing Dynamic Code Splitting and Hot Updates in React Native with Metro Bundler
This article explains how to analyze React Native bundle contents, customize Metro's module ID generation to achieve stable code splitting, and integrate the resulting base and feature packages with a hot‑update workflow that preloads the base bundle, loads feature bundles on demand, and handles loading failures on iOS.
Dynamic code loading is a core feature of React Native, and as business logic grows the monolithic bundle becomes too large, causing excessive traffic for hot updates; therefore splitting the bundle into a stable base package and smaller feature packages is essential.
React Native uses Metro to bundle JavaScript; the generated index.ios.bundle consists of three parts: polyfills (environment variables and require definitions), module definitions (the define calls), and module execution (the __r calls).
Example component:
import { StyleSheet, Text, View, AppRegistry } from "react-native";
class RNDemo extends React.Component {
render() {
return (
RNDemo
);
}
}
const styles = StyleSheet.create({
container: { height: 44, position: 'relative', justifyContent: 'center', alignItems: 'center' },
hello: { fontSize: 20, color: 'blue' },
});
AppRegistry.registerComponent("Soul_Rn_Demo", () => RNDemo);Bundling command (iOS example):
npx react-native bundle --platform ios --dev false --minify false --entry-file RNDemo.js --bundle-output index.ios.bundleThe polyfill section defines global helpers such as global.__r = metroRequire and global.__d = define , which are the entry points for module execution and definition. The metroRequire function resolves a module ID, loads the module (including native modules when needed), and executes its factory function.
Module definition is performed by the define function, which creates a module object and stores it in the global modules map. The generated bundle shows a call like __d(function (global, _$$_REQUIRE, ...) { ... }, 0, [1,2,3,7,9,10,12,194]); , linking the module code with its dependencies.
By default Metro creates module IDs with an incremental factory ( createModuleIdFactory ) that starts at 0; however, changing import order changes the IDs, breaking compatibility between old base bundles and new feature bundles.
To keep IDs stable, a custom createModuleIdFactory writes the path‑to‑ID mapping into ./soulCommonInfo.json and re‑uses it on subsequent builds, only assigning new IDs for previously unseen files. The implementation reads the JSON cache, restores the map, and writes new entries when needed:
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
const file = "./soulCommonInfo.json";
const stats = fs.statSync(file);
if (stats.size === 0) { clean(file); }
else { const cache = require(file); if (cache instanceof Object) { for (const [path, id] of Object.entries(cache)) { nextId = Math.max(nextId, id+1); fileToIdMap.set(path, id); } } }
return (path) => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") { id = nextId++; fileToIdMap.set(path, id); if (!hasBuildInfo(file, path)) { writeBuildInfo(file, path, fileToIdMap.get(path)); } }
return id;
};
}
module.exports = { serializer: { createModuleIdFactory } };With stable IDs, the split‑package strategy can be applied: the base bundle is pre‑loaded once, and each feature bundle is loaded on demand via the native bridge. On iOS the base bundle is set through sourceURLForBridge ; additional bundles are executed with RCTCxxBridge.executeSourceCode , which requires exposing the private method via a Category.
Loading completion is detected by listening for RCTJavaScriptDidLoadNotification for the base bundle and by having the JS side explicitly notify the native side after a feature bundle registers its page, ensuring reliable error handling.
If the base bundle is not yet ready when a page is requested, the request is cached and the base bundle loading is triggered; once the base bundle finishes, the cached page is opened. Visual flow diagrams illustrate the loading order, fallback mechanisms, and hot‑update process.
The overall hot‑update workflow includes an embedded base bundle (to improve launch success), a fallback feature bundle, signed and compressed feature bundles fetched from a custom build and gray‑release platform, and automated CI/CD pipelines that deliver updates to the app.
In summary, the article presents a complete solution for dynamic code splitting, stable module ID generation, and hot‑update integration in a large‑scale React Native application, and outlines future work on performance and stability improvements.
Soul Technical Team
Technical practice sharing from Soul
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.