A Dive into SystemJS – Production Considerations

Previously we have looked at the basic configuration of SystemJS and what happens when you attempt to load modules. What we have covered so far is good enough for a development system, but things are different when you try to push your code to production and performance is much more important. It might be fine for a development system to make XHR requests for each individual script file, but that is not ideal for most production systems. This article will attempt to evaluate the production setup that is needed to attain good performance.

This is the last of three articles about SystemJS. If you are looking for the basics then check out part 1, and if you want to read about loading and translating modules then check out part 2.

Production Workflows
If you were to deploy an application using only what we have covered so far your app would be making individual requests for each dependency which is not going to result in good performance. SystemJS provides for a couple of different approaches that you can take to optimize this performance depending on what type of environments that you need to support.

Bundling
The first optimization technique is bundling. Guy Bedford, the creator of SystemJS, also published a Node module, systemjs-builder, that you can install and use to perform bundling on your application.

This library is pretty easy to use for basic bundling situations. You simply create an instance of the bundler and either give it a reference to your SystemJS config file or specify a custom configuration, and then bundle a file.

var Builder = require('systemjs-builder');

// set it up with your base URL and the path to your config file.
var builder = new Builder('/baseUrl', 'config.js');

// you can also override configuration settings
builder.config({
    map: {
        jquery: 'customjQuery.js'
    }
});

// run the bundler
builder.bundle('myBootstrap.js', 'bundles/bootstrap.js').then(function(output) {
    // do anything that you need to do post-bundle
});

The bundler also supports transpiling and the various minification steps, compressing and mangling, that can be enabled via an optional parameter. If either of those are enabled then you also have the option of having the bundle include the source maps to map your bundle back to your original source code. One thing to remember is that if you turn on minification then mangling is also enabled by default, so if you just want whitespace compression then you need to turn minification on and turn off mangling.

// run the bundler with just whitespace compression and source maps
builder.bundle('myBootstrap.js', 'bundles/bootstrap.js', { minify: true, mangle: false, sourceMaps: true }).then(function(output) {
    // do anything that you need to do post-bundle
});

The bundler also supports a multi-tiered bundle output so if you want to use multiple bundles then you can. Let us consider an example where you have two different bootstrap files that share some subset of dependencies. In that case you might want a bundle for the shared dependencies that could be cached by the browser independently and then a separate bundle for each bootstrap.

// calculate the shared dependencies
builder.bundle('myBootstrap1.js & myBootstrap2.js', 'bundles/commonDependencies.js').then(function(output) {
    // now build the bootstrap bundles
    builder.bundle('myBootstrap1.js - bundles/commonDependencies.js', 'bundles/bootstrap1.js').then(function(bootstrap1Output) {});

    builder.bundle('myBootstrap2.js - bundles/commonDependencies.js', 'bundles/bootstrap2.js').then(function(bootstrap2Output) {});
});

The bundler also works with file globs so you can bundle whole folders or all files matching a path and you can also use those when performing bundle arithmetic.

// build a bundle with all of the application code except for the bootstraps and any external libraries
builder.bundle('src/**/* - src/bootstraps/* - jspm_packages/**/*', 'bundles/applicationCode.js').then(function(output) {});

With normal usage the bundler will include the module and all of its dependencies. However, if you just want to include the module itself and ignore its dependencies then you can use the module syntax of [].

// build a bundle with just the bootstrap files and none of their dependencies
builder.bundle('[src/bootstraps/*]', 'bundles/bootstraps.js').then(function(output) {});

You do not have to always specify the output file when you run a bundling attempt. If you do not specify it then it will perform the bundle in-memory and let you handle the bundled source however you like.

// build a bundle with just the bootstrap files and none of their dependencies
builder.bundle('[src/bootstraps/*]').then(function(output) {
    // output.source will contain the source of the bundled code.
    // output.sourceMap will contain the source map of the bundle.
    // output.modules will contain an array of module names that were included in the bundle.
});

Using Bundles in the Browser
Once you have created the bundles that you want to use now you will need to configure SystemJS to make it aware of your bundles. This is as easy as defining the path to the bundle with an array of module names that are included in the bundle.

// SystemJS config
// The key is the path to the bundle, the value is the array of modules in the bundle
bundles: {
    'bundles/bootstrap.js': ['src/bootstraps/bootstrap.js', 'src/someDependency1.js', 'src/someDependency2.js']
}

The way that SystemJS handles bundles is that when you attempt to import a module, before SystemJS goes out and fetches it it will first check to see if that module was defined as being included in a bundle. If it is, then it will go out and fetch the bundle instead of the individual module. This comes in handy for a developer because all you have to do to switch back to loading files individually is comment out the bundle definition in the config file and all that you have to do to make your app production ready is to add the bundle definition.

// importing one of the bundled modules will result in the bundle getting loaded
System.import('src/bootstraps/bootstrap.js').then(function(bootstrap) {
    bootstrap.start();
});

Tracing
If your bundling needs are complicated enough then you might want to use tracing to build your dependency tree and then implement your own bundling logic. Tracing performs the same dependency discovery logic that bundling does, but it does not create the actual bundle and just gives you a dependency tree that you can do whatever you want with. For instance, you could use this to build a report of your application’s structure and dependencies, or you could use it to build your dependency tree and then run some algorithm to figure out the ideal bundles.

// use the same builder as bundling
builder.trace('src/bootstraps/bootstrap.js').then(function(tree) {
});

The tree object returned by tracing has a key for each module that it found in the trace attempt. The data for each module is quite extensive. It contains a metadata section which has some of the meta information about the module, such as the loader that loaded the module (in case you use a loader plugin), the source map, the original source, any dependencies defined in your SystemJS config for this module, and also the module format. In addition to the metadata it will also include an array of the module imports that this module registered, and then it will also include a depMap that converts those imported names to the module names after the SystemJS map configuration is applied.

// the deps and depMap portion of the tree output
deps: [
    'components/helloWorld',
    'jquery'
],
depMap: {
    'components/helloWorld': 'src/components/helloWorld.js',
    'jquery': 'npm:jquery@2.2.0'
}

This seems like it could be useful for generating a report or a dependency tree, but how does this help you with bundling? Well, one option would be to use this to figure out which files should be included in a given bundle and then run the bundling tool using bundle arithmetic to create the correct bundles. However, SystemJS also gives you a tool to figure out the common modules from multiple trees. If you run a trace on multiple files and get the multiple trees then you can pass them two at a time to the intersectTrees method and get back a new tree that contains only the common modules. This tree object can then be passed to the bundler and it will generate a bundle from the tree.

Promise.all([builder.trace('bootstraps/bootstrap1.js'), builder.trace('bootstraps/bootstrap2.js')]).then(function(trees) {
    var sharedDependencies = builder.intersectTrees(trees[0], trees[1]);
    builder.bundle(sharedDependencies, 'bundles/sharedDeps.js');
    builder.bundle(builder.subtractTrees(trees[0], sharedDependencies), 'bundles/bootstrap1.js');
    builder.bundle(builder.subtractTrees(trees[1], sharedDependencies), 'bundles/bootstrap2.js');
});

As you can see from this example the SystemJS builder also exposes arithmetic functions for trees.

HTTP/2
Modern browsers and modern web servers also support the HTTP/2 protocol, information on which can be found here. One advantage of HTTP/2 is that it can combine multiple requests to the same domian into a shared connection so then you do not really need to bundle as much. The benefit of doing less bundling is that the browser can cache and reuse the individual script files and only download the scripts that have changed. With bundles the browser will need to download the entire bundle again if one of the scripts inside of it has changed.

SystemJS supports HTTP/2 with the depCache configuration property. The depCache property functions in a similar manner to the bundle, except that instead of loading the bundle when one of the modules is requested it will actually load each of the individual dependencies in parallel to take advantage of HTTP/2.

// SystemJS config
// The key is the module and the value is the array of dependencies for that module
depCache: {
    'src/bootstraps/bootstrap.js': ['src/someDependency1.js', 'src/someDependency2.js'],
    'src/someDependency1.js': ['src/someSharedDependency.js'],
    'src/someDependency2.js': ['src/someSharedDependency.js']
}

// This will trigger loads of bootstrap.js, someDependency1.js, someDependency2.js, and someSharedDependency.js all in parallel
System.import('src/bootstraps/bootstrap.js');

You should note however that not all browsers or servers support HTTP/2 so you will need to carefully consider how you want to deliver your production scripts based on what browsers and servers you will be supporting.

CSP
Some sites might require that your scripts are CSP compatible. If you are not familiar with CSP then here is a good explanation. If you are building your site as a CSP compatible site then you will need to make sure that all of your code will work if it is loaded with script tags. There is a special build of SystemJS that uses script tag injection to perform imports, and there are configuration options for bundling that will create CSP compatible bundles.

The CSP compatible bundling approach creates what is called a self-executing bundle. This is a bundle that is fully self-contained with no other dependencies and is an IIFE so that it immediately executes once the script finishes loading. If we look at our previous bundling examples, it is just a small change to make it CSP compatible.

// build a self executing bundle
builder.bundleStatic('src/bootstrap.js', 'bundles/bootstraps.js').then(function(output) {});

// SystemJS config
bundles: {
    'bundles/bootstraps.js': ['src/bootstrap.js', 'src/someDependency.js']
}

// If you use the CSP compatible System implementation then when you do this import it will use script tags.
System.import('src/bootstrap.js');

Conclusion
Different applications will have quite different requirements for their production workflows, and SystemJS tries to provide for those different cases. Whether you need to bundle or whether you want to live life on the edge and rely solely on HTTP/2, SystemJS has you covered. I hope that this article series has given you a solid understanding of how SystemJS works and some of the different options that you have available to you for delivering your code in production.

Leave a Comment