Unbundling React Native Unbundle

I started looking into what was needed to support unbundle on react-native-windows. A react-native “unbundle” is a lazy loading mechanism for your JavaScript modules. Each module is loaded on-demand when it’s first needed, the goal being to reduce the time it takes to start a React Native app, and reduce the memory consumed by potentially unused code. For details on how we added unbundle to react-native-windows, checkout Hacking Unbundle into React Native for UWP (coming soon).

React Native Bundle Varieties

There are four kinds of bundles you might encounter in React Native.

Plain JavaScript Bundle

This is your run-of-the-mill bundle, the one that gets generated with the react-native bundle command. It comes in either a dev or release flavor, with the primary difference being that the release flavor is optimized and minified. After building the dependency graph of all modules referenced in your app, a bundle is effectively a concatenation of all these modules.

File-based Unbundle

This is the version of unbundle that is used by Android by default. Given all the modules in your app, a file-based unbundle is made up of an entry point file and a folder called js-modules that includes a file-per-module for each of the modules in your dependency graph. Each module is assigned an index, and all require calls in your app are rewritten from a module name to the module index assigned to that module. There’s also a magic file called UNBUNDLE in the js-modules folder that signals to React Native that your app bundle is an unbundle, and the app should configure itself to load modules on-demand.

Indexed Unbundle

This is the version of unbundle used by iOS by default, but can also be used on Android. On iOS, the cost of file IO outweighs the potential savings of loading JavaScript modules on-demand from assets, so the indexed unbundle solves this problem by putting all the contents of the unbundle in a single file. The file has a binary header that includes the module table, with information on the offset and length of each module in the file, and the information about the number of bytes in the startup code. The single file approach can be paged into RAM, so fewer costly disk IO operations are required.

Bytecode Bundle

To be quite honest, I haven’t done a lot of research on the byte-code bundle for React Native, but I understand the concept in general. Many JavaScript engines, such as those used in React Native, parse JavaScript into a bytecode, which can then either be interpreted by a virtual machine or JIT compiled into machine code. The concept of the bytecode bundle is that the JavaScript bundle is pre-parsed into the bytecode used by the JavaScript engine on the device. This saves valuable cycles at app startup time by eliminating the parsing step. In react-native-windows, we implemented a concept similar to the bytecode bundle that leverages ChakraCore’s JsSerializeScript and JsRunSerializedScriptWithCallback APIs.

Generating React Native Bundles

Generating bundles for React Native can be done with the react-native-cli.

To generate a plain JavaScript bundle:

  react-native bundle [options]
  builds the javascript bundle for offline use


    -h, --help                         output usage information
    --entry-file <path>                Path to the root JS file, either absolute or relative to JS root
    --platform [string]                One of "ios", "android", or "windows"
    --transformer [string]             Specify a custom transformer to be used
    --dev [boolean]                    If false, warnings are disabled and the bundle is minified
    --bundle-output <string>           File name where to store the resulting bundle, ex. /tmp/groups.bundle
    --bundle-encoding [string]         Encoding the bundle should be written in (https://nodejs.org/api/buffer.html#buffer_buffer).
    --sourcemap-output [string]        File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map
    --sourcemap-sources-root [string]  Path to make sourcemap's sources entries relative to, ex. /root/dir
    --assets-dest [string]             Directory name where to store assets referenced in the bundle
    --verbose                          Enables logging
    --reset-cache                      Removes cached files
    --read-global-cache                Try to fetch transformed JS code from the global cache, if configured.
    --config [string]                  Path to the CLI configuration file

The only options that are required are --entry-file, --platform, and --bundle-output. Most apps will also need to use --assets-dest if the app has any images, HTML files, or other assets that are referenced using require().

Generating an unbundle is no different than generating a bundle. The command line options are identical except for one additional option --indexed-unbundle.  Since an indexed unbundle is the default and only supported behavior for iOS, you would only use this flag if you wish to have an indexed unbundle on Android (or Windows!).

I would definitely recommend evaluating the performance of your app when using unbundle versus a standard bundle before committing to unbundle. For smaller apps with relatively few modules (say, a few hundred small modules), the bundle size may only be a few MBs, and the overhead of many more IO operations may outweigh the benefits of loading the entire bundle at app startup. You may also find that the penalty for IO occurs at an unacceptable point in your app, but there are many workarounds to ensure critical modules are loaded in advance from an unbundle.

Happy (un)bundling!

Unbundling React Native Unbundle