Aurelia CLI and RxJS - Size sensitive bundles

I'm starting a new blog series about best practices with an Aurelia CLI TypeScript app and RxJS. For everybody new to RxJS I can highly recommend this Egghead Video by André Staltz which will give you a headstart with this incredible framework.

This first article will show you how to integrate a minimal version of RxJS with a new Aurelia CLI TypeScript app. The primary concern is bundle size, so before we dive into the topic lets examine what a new scaffold looks like in the begin.

You can find the resulting app over at this github repository which will be used for further examples and articles. This tag marks the snapshot of features of this article.

Creating the Demo app

We'll scaffold a new Aurelia application with TypeScript using the cli command au new [name-of-your-app].
We're greeted with a nice walkthrough wizard where we pick RequireJS (1) and TypeScript (2). We'll use maximum minification (3) since we're after a small bundle size right?
This demo ain't about CSS nor testing so pick no for both of them (1, 2).

Make sure to use the latest version of the Aurelia CLI, currently v.0.31.0, which makes it possible to trace dependencies without a main entry point

Aurelia Weightwatchers

Now lets start the numbers game.
Running au build --env prod will yield a production ready app and vendor bundle, which will utilize all the minification hocus pocus for bundled templates. This is what you're gonna distribute once your app is ready to be released. And currently it weights 395kb. I know what you're thinking now. Isn't there a magic number of around 200kb you should stay under in order to provide your content fast even for slow connections? Keep in
mind that the default example uses the defaultConfiguration as seen in the main.ts configure method:

...
export function configure(aurelia: Aurelia) {  
  aurelia.use
    .standardConfiguration()
    ...

What might look like a small call actually translates to:

...
export function configure(aurelia) {  
  aurelia.use
    .defaultBindingLanguage()
    .defaultResources()
    .history()
    .router()
    .eventAggregator();
  ...

So lets say we're not into routing for our small demo app, nor do we need an eventAggregator and HTML5 history support.
By switching the configure function to read like this:

...
export function configure(aurelia) {  
  aurelia.use
    .defaultBindingLanguage()
    .defaultResources()
  ...

and remove the following lines from aurelia.json

...
dependencies: [  
  ...  // remove the following lines scattered throughout the bundles dependencies array
  "aurelia-event-aggregator",
  "aurelia-history",
  "aurelia-history-browser",
  "aurelia-route-recognizer",
  "aurelia-router",
  {
    "name": "aurelia-templating-router",
    "path": "../node_modules/aurelia-templating-router/dist/amd",
    "main": "aurelia-templating-router"
  },
]

and run au build --env prod again we get a vendor bundle with a size of 347kb. Cool saved 48kb minified vendor code not necessary for our demo. Keep in mind that not every app really needs a router so this might be something for your next application as well. But if you're like me it still feels like there is stuff left to do.

Reducing even more weight

Our demo neither needs super duper nice support for Edge nor IE11 since everybody uses modern evergreen browsers right?
So lets strip out all the bluebird related stuff as well for proper Promises.

"node_modules/bluebird/js/browser/bluebird.core.js",
"node_modules/aurelia-cli/lib/resources/scripts/configure-bluebird.js",

And running the build command again we get 295kb. Another 52kb saved. We're getting somewhere.

Last but not least lets see how big the size is for our clients requesting the app. In order to reduce that size even more we can use gzip to compress our data size when delivering over the wire. Since we're not gonna create a dedicated server lets use the CLIs built in BrowserSync and add gzip support.

To do so first run npm install --save-dev compression to save the gzip package as dev dependency. Next update your aurelia_project/tasks/run.ts file by adding the top import and activating compression as middleware.

import * as compress from 'compression'; // <-- add this

let serve = gulp.series(  
  build,
  done => {
    browserSync({
      online: false,
      open: false,
      port: 9000,
      logLevel: 'silent',
      server: {
        baseDir: [project.platform.baseDir],
        // add compress() to the list
        middleware: [compress(), historyApiFallback(), function(req, res, next) {
          res.setHeader('Access-Control-Allow-Origin', '*');
          next();
        }]
      }
    }, function (err, bs) {
      if (err) return done(err);
      let urls = bs.options.get('urls').toJS();
      log(`Application Available At: ${urls.local}`);
      log(`BrowserSync Available At: ${urls.ui}`);
      done();
    });
  }
);

Fire up the dev server in production mode with au run --env prod and start your devtools. Switch over to the network tab et voila ...

Devtools showing the vendor.bundle size using gzip

72.1kb. Now we're talking. That's something I feel quite comfortable to distribute as part of my vendor bundle.

Wasn't this about RxJS?

Right, the purpose of this article was to take a look at how to bundle RxJS with an Aurelia CLI app. Lets do so by installing rxjs via npm install --save rxjs and adding the following dependency config to our aurelia.json file:

dependencies: [  
  ...
  {
    "name": "rxjs",
    "path": "../node_modules/rxjs",
    "main": "Rx"
  }
]

Running the build command again we get ... wait WHAAT 547kb? Jesus Christ, at this point we might should start thinking whether FRP and all this hype surrounding Observables is really necessary. I mean from 295kb to 547kb, thats pretty much the same as the whole Aurelia bundle including the RequireJS runtime.

Yep RxJS is a little fatty. He ate too much observables and subjects when he was a child, and continued stuffing additional operators and helpers when he grew up. Now being in v5 it gets even messier. But looking at the bundled Rx.js we see that it contains all sorts of stuff.

require('./add/operator/dematerialize'); what the heck is that? Oh the counterpart to require('./add/operator/materialize');. Are we really gonna need a WebSocketSubject either? So wouldn't it be nice if we could only bundle what we really need?

YES WE CAN, AND YES WE SHOULD !!!

In order to do so we'll simply have to update the above dependency configuration to say "main": false. Running the bulid command we see we're back at 295kb.

Granular (patched) RxJS imports

The above settings simply tells the Aurelia CLI not to use any default file and thus start importing things. As long as there's no reference we won't need to bundle anything.
What we need to make sure of when requesting features from RxJS though is that we do not import the full library itself anywhere. That happens through one of the following statements

import * as rx from 'rxjs';  
import { Observable, ... } from 'rxjs';  
import 'rxjs';  
import 'rxjs/Rx';  

As soon as we do so, we'll get all the fatty size back in. Luckily above won't happen since the barrel export is called Rx.js. Having main set to false an import from 'rxjs' would be translated to 'rxjs/index' and since that doesn't exist we'll just get an error.

For the demo lets update the app.ts file as follows:

import { Observable } from 'rxjs/Rx';

export class App {  
  message = 'Hello World!';

  attached() {
    Observable.of(3 * 7)
      .map((value) => value * 2)
      .subscribe(
        (result) => this.message = result.toString()
      );
  }
}

As we can see the observable inside attached requests the Observable type and the of and map operators. So in order to make sure to only bundle what we really need just add the following imports instead.

import { Observable } from 'rxjs/Observable';  
import "rxjs/add/operator/map";  
import "rxjs/add/observable/of";  

This might sound confusing but if you can't remember how the import paths work just try to speak out those import statements loud.
First one says import X from rxjs/X, where X is the thing we want, namely the Observable. Next we say add the operator map and last but not least add the observable method observable.of. Remembering that makes it easier to write your import statements, alternatively navigate to node_modules/rxjs and look around for what you're searching for.

Back to why we're doing all of this ... running the build command we end up with ... drum rolls ... bells whistle ... aaaaaaaand 309kb. Running that with gzip results in 75.3kb which is the way it should be.

Conclusion

I hope you saw that Aurelia and especially RxJS at the begin look like fat giant libraries. But by sorting out what you don't need and applying gzip you can get the bundle down to a really decent size. Hope you enjoyed the post and if there are any questions don't hesitate to leave your comments below.

photo credit: TeroVesalainen: Weight Loss via Pixabay (license)