Production Code Splitting With Bazel

Rollup has recently added an experimental code splitting feature as of version 0.55.0 (https://github.com/rollup/rollup/pull/1841). I updated the Bazel rules_nodejs production bundling rollup_bundle rule to make use of this feature and this blog post describes how this rule works with code splitting.

Code splitting allows you to split application code into various bundles. These can be loaded on demand (lazy loaded) or in parallel. This approach can reduce the load time of your application significantly. When used with Angular, as in this demonstration, the parts of your application that are not needed during the initial load can be lazy loaded when the user navigates to a specific routes or pre-loaded in the background after the initial application is started.

Bazel is a Google open-source project. It is a fast, scalable, multi-language build system that is based on Google’s internal build system used to build nearly all of Google’s applications.

Disclaimer

Code splitting with rollup_bundle is currently meant as a demonstration only as it has not yet been released and breaking changes will likely be made before it is. Further, the development server, ts_devserver, rule does not yet support code splitting so making use this rule for code splitting will mean that you’ll be debugging on the rollup_bundle output unless you use a custom development server. Code splitting support for ts_devserver is currently in the design phase and will be coming in the future.

Code

The demonstration is built on a fork of Alex Eagle’s canonical angular-bazel-example on this branch: https://github.com/gregmagolan/angular-bazel-example/tree/rollup-code-splitting.

The updated rollup_bundle rule is pulled from my fork of rules_nodejs: https://github.com/gregmagolan/rules_nodejs/tree/rollup-code-splitting.

Configuration

The configuration for the rollup_bundle rule does not change much for code splitting.

Previously it was:

rollup_bundle(
name = "bundle",
entry_point = "src/main",
deps = ["//src"],
)
nodejs_binary(
name = "prodserver",
args = ["./src"],
data = [
"index.html",
":bundle",
":zone.js",
],
entry_point = "http-server/bin/http-server",
)

Rollup would produce a minified bundle file and the prodserver target would serve it and all other files required using http-server. You can try out the current rollup_bundle without code splitting on Alex Eagle’s canonical angular-bazel-example.

With code splitting, the configuration is updated as follows:

rollup_bundle(
name = "bundle",
index_html_template = "index.template.html",
entry_points = [
"src/main",
"src/foo/foo.module.ngfactory",
],
deps = ["//src"],
)
nodejs_binary(
name = "prodserver",
args = ["src"],
data = [
":bundle",
":zonejs",
],
entry_point = "history-server/modules/cli.js",
)

You’ll notice that entry_point has been renamed to entry_points and now takes an array of entry points, with the first being the initial entry point for the application. Also, index.html is no longer a static file provided to prodserver, but rather a templated file. The rollup_bundle rule will expand this template with the necessary systemjs configuration required for code splitting and output an index.html file that is served by the prodserver target.

The lazy loaded entry points (src/main, src/foo/foo.module.ngfactory) need to be configured manually in the rollup_bundle rule for the time being. In the future, these will be automatically derived from metadata generated at build time.

The prodserver target has also been updated to use history-server instead of http-server as this supports lazy loading with the Angular router.

Usage

On my rollup-code-splitting branch of angular-bazel-example, the production server is started with bazel run src:prodserver.

Bazel will build the code split bundles and run the prodserver, which listens on port 8080.

INFO: Running command line: dist/bin/src/prodserver src
history-server listening on port 8080; Ctrl+C to stop

In Chrome devtools, when you navigate to http://localhost:8080, you’ll see that the browser loads zone.min.js and system.js. These scripts are required for the application to run. It then loads main.js script, which is the main entry point to the application which was bundled by rollup. main.js will request the common chunk that it requires, chunk1.js, which is loaded as well.

At this point the application has everything is needs to run the main page but it hasn’t yet loaded the script for the `/foo` route. Only when you click on the Foo link does Angular request the foo.module.ngfactory.js script required for that route. The browser then loads foo.module.ngfactory.js and Angular navigates to the /foo route. The foo.module.ngfactory.js bundle also requires the common chunk chunk1.js but since it has already been downloaded by the browser it is not requested.

Note that the behavior is different if you manually change the URL to http://localhost:8080/foo instead of clicking on the Foo link which tell Angular to change the route to /foo. In this manual case, the browser does a full reload and all the scripts are reloaded in the same order and the application is refreshed.

Deep Dive

Lazy Loading Configuration in index.html

The templated index.template.html that is passed to rollup_bundle contains a TEMPLATED_rollup_scripts tag. The rollup_bundle rule expands this tag into:

<script src="/system.js"></script>
<script>
(function (global) {
System.config({
packages: {
'' : {
map: {"./main": "bundles.es5_min/main", "./foo/foo.module.ngfactory": "bundles.es5_min/foo.module.ngfactory"},
defaultExtension: 'js'
},
}
});
})(this);
</script>
<script>
System.import('main').catch(function(err){ console.error(err); });
</script>

It adds these script tags, which configure systemjs in the browser to load the lazy loaded bundles when they are requested by the application, to the generated index.html. The rollup_bundle rule also outputs the system.js file so it is available for the prodserver to serve.

Rollup Configuration

The rollup_bundle rule produces a few different types of code split bundles during the build. For the purposes of this demonstration, these are all outputted. In the future, only bundles used by downstream rules would be generated.

First rollup is run on the ES6 compiled application sources and it outputs ES6 code split bundles to the bundles.es6 folder in your bazel-out folder. Next typescript is run to downlevel these to ES5 into the bundles.es5 folder. Finally, uglify is run twice, once without--beautify and once with --beautify and it outputs into the bundles.es5_min and bundles.es5_min_debug folders respectively. The generated index.html is configured by the rule to use the bundles.es5_min output, which is non-debug minified production output.

The experimental rollup code splitting feature only supports the cjs output format presently, so the rollup output is in cjs.

Rollup does not currently provide control over the output filenames. These will correspond to the entry point filenames: main.js and foo.module.ngfactory.js in the angular-bazel-example demonstration. Rollup outputs common code between entry point bundles into one or more chunkX.js files. In the angular-bazel-example, there is only one common chunk: chunk1.js. In larger applications there would be more common chunks, with each entry point bundle loading only the common chunks that it requires.