Back to Blog Home
← all posts

Improving App Startup Time on Android with Webpack + V8 Heap Snapshot

July 5, 2017 — by Ivan Buhov

Startup time is a critical success factor for modern mobile applications. Therefore, we are constantly striving to minimize the time spent in the initialization phase of NativeScript apps. With the latest release of our nativescript-dev-webpack plugin, we are taking a big step forward in optimizing the startup time on Android by introducing V8 heap snapshot support.

We measured 30% startup time improvement of an empty Angular application on a Nexus 5 device. Adding more code to the snapshotted bundle leads to even better results. Our SDK Examples app starts 40% faster with snapshot enabled. The percentage is even higher on older devices. We are excited to give you this feature and would love to hear from you when you measure the startup time improvements in your applications.

V8 Heap Snapshot

The V8 JavaScript engine has a valuable feature called custom startup snapshots - giving an opportunity to all V8 embedders, including the {N} Android runtime, to initialize a JavaScript context with a previously prepared heap snapshot. In other words, instead of fetching, parsing, and executing a script on every startup, we can execute the script just once and serialize the V8 heap state in a binary blob file. Next, the generated blob is included in the APK bundle and loaded by the Android Runtime on startup. As a result, the app starts notably faster without sacrificing any functionality.

How to use it

V8 heap snapshotting support is brought by the NativeScriptSnapshot Webpack plugin included in the latest nativescript-dev-webpack package. If you still don’t use Webpack bundling, this article can help you set up your initial configuration. In case you already have existing Webpack configuration, make sure you update it after installing the latest nativescript-dev-webpack package:

node ./node_modules/.bin/update-ns-webpack

In case, you have nativescript-dev-android-snapshot package installed - you can safely remove it. It is obsolete and will be deprecated soon:

npm uninstall nativescript-dev-android-snapshot

Now, you should have the following lines of code in your webpack.config.js:

...
    if (env.snapshot) {
        plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({
            chunk: "vendor",
            projectRoot: __dirname,
            webpackConfig: config,
            targetArchs: ["arm", "arm64", "ia32"],
            tnsJavaClassesOptions: { packages: ["tns-core-modules" ] },
            useLibs: false
        }));
    }
...

Next, all we need to do is to pass a --snapshot flag to the Android bundling command in our package.json file and run the command:

To enable snapshot generation pass --snapshot flag to the android bundling commands in package.json

You can see the exact arguments passed to the underlying snapshot generator in the log produced by the bundling step:

Snapshot generator arguments

The snapshot generation feature is limited to macOS and Linux platforms due to inability to build mksnapshot tool running on Windows. Currently, the --snapshot flag is a no-op on Windows.

How it works

Under the hood, our snapshot generator uses the so-called mksnapshot tool developed by the V8 team, to produce a snapshot blob from an arbitrary script. This is done by executing the script in a completely empty V8 instance, which then saves the heap state in a binary blob file.

Snapshot generation process diagram

Therefore, the V8 context in which the script is executed has no additionally injected APIs except those coming with the JS engine. However, the V8 context provided by the Android runtime, where the generated blob is loaded on app startup, is quite rich with additionally injected APIs:

  • Native Java APIs exposed to the JS world (java.lang.Object etc.)
  • APIs specified by the CommonJS modules spec (require, module, exports etc.)
  • Runtime specific APIs ( global.__runtimeVersion, global.__extends etc.)

These APIs are exposed by the Android runtime which makes them unavailable in the snapshot context. Any attempt to call them in snapshotted script will throw an error. Another limitation is that a V8 context can be initialized with a single pre-generated heap snapshot. In other words – only one script file can be snapshotted.

To overcome the lack of a CommonJS modules notion in the snapshotted context, and to make our single snapshotted script as large as possible, our best bet is to use a JavaScript bundler with CommonJS modules support. Since Webpack is widely used among the community, the choice wasn’t hard to make.

Unfortunately, Webpack can’t overcome the unavailability of Java and runtime specific APIs in the snapshotted context. Therefore, you may receive reference errors if an API injected by the Android runtime is touched during snapshot generation:

snapshot-generator-error

Modules included in the snapshotted bundle can still contain native API calls given that they are not evaluated immediately upon module loading. For example, the following module:

require("application");

var time = new android.text.format.Time();

can’t be snapshotted because it touches android.text.format.Time API which is not available. However, in this one:

require("application");

function getTime() {
    return new android.text.format.Time();
}

the native API access is not evaluated on module execution. Given that getTime() is called later in the fully featured V8 context provided by the Android runtime, we are safe to include the module in the snapshotted bundle.

If the snapshotting step fails because of a reference to an undefined API, try some of the following solutions:

  • If you can change the module containing the forbidden API call, wrap the guilty code in a function that is called once the app is running on device
  • Keep the module in the bundle but make sure all require calls of the non-snapshotable module are executed once the app is running on device:
    require("application");
    var m = require("non-snapshotable-module");
    
    function doSomething() {
        return m.someMethod();
    }

    The code above has a higher chance to be successfully snapshotted if it loads the non-snapshotable module when it actually needs it:

    require("application");
    
    function doSomething() {
        return require("non-snapshotable-module").someMethod();
    }

    If doSomething() function is never called in snapshot context, the non-snapshotable module will not be evaluated and blob generation will succeed.

  • Exclude the module containing the forbidden API call from the snapshotted bundle.

All we need to know about CPU architectures

The V8 library shipped with the Android runtime contains 3 CPU architecture slices - ia32 (for emulators), arm and arm64 (for devices). Since V8 heap binary format is not architecture-agnostic, we need a different blob file for each CPU architecture. Generating snapshot for all of them guarantees that the right blob file will always be found on app startup. However, when our build targets a subset of all supported architectures, it is a good idea to enable heap generation for only those that are included in the subset. For example, when building for devices, we can exclude ia32. Additionally, arm64 can also be removed from the list because, if not explicitly specified, arm binaries are used even on arm64 devices, which makes the arm64 blob file useless.

Including unnecessary architectures doesn’t hurt app performance but does increase app package size. Targeted architectures can be configured by modifying the targetArchs option of NativeScriptSnapshot Webpack plugin. Check out the docs to find out more details on how to configure the snapshot generation process.

V8 heap snapshots – the past and the future

As you may know, V8 heap snapshot support is currently available through the nativescript-dev-android-snapshot plugin. Starting from v3.1.1 it is no longer part of the default template and will be deprecated soon in favor of nativescript-dev-webpack integration with locally generated snapshots. Here is a list of drawbacks we have encountered in the old approach:

  1. Using only predefined set of snapshotted packages: tns-core-modules and tns-core-modules + nativescript-angular.

  2. No control over what is included in the snapshot:

    • Can’t include client code.

    • Can’t include other packages/plugins.

  3. No support for Angular ahead of time compilation + snapshot in case of Angular application.

  4. You are forced to choose between Webpack and snapshot. Can’t have both.

  5. In many cases Webpack gives a better result than snapshot, e.g. in Angular applications Webpack + Angular ahead of time compilation leads to better startup time then using snapshots.

  6. Hard to debug and troubleshoot errors because snapshot generation doesn’t happen on your machine.

All these downsides pushed us to make V8 heap snapshot generation happen locally on the developer machine as next step after code bundling. This also unifies our performance optimization path giving you the opportunity to use all available techniques (Webpack, AoT compilation, uglify, snapshots etc.) instead of choosing a subset, because of existing incompatibilities.

Give your feedback

We are happy to hear your feedback on local snapshot generation feature. Don’t forget to share with the community your startup time measurements after enabling it. You can log issues in nativescript-dev-webpack repository or even post a pull request if you really want to be involved.