Update:
This post was written just after Angular's version 2.0 release. In that era the Angular CLI project was just getting started. Today they provide a complete build package so you should go over to their site if you want to get started right away.
Disclaimer: As commenter Ben Elliot pointed out, the UglifyJs plugin used in the Webpack build is not able to remove unused Typescript classes because those, when transpiled, are implemented using IIFE. The bundle size mentioned in this post is not really reflecting tree shaking but minification. To be able to properly apply tree shaking we need to use a different module loader like Rollup. The repository referenced in this post has been updated to use Rollup instead of Webpack 2 with a resulting bundle file size of 522 KB. A new blog post will be added in the future explaining the new workflow.
Angular 2 is massive framework for creating modern web applications. Just to create a "hello world" application, we might end up with a bundle of more than 2.5 MB. If you have a 50 mbps internet connection, that might not seem problematic but most people don't have that privilege. What's more, Angular 2 has now morphed into a multi-platform framework with the goal of creating mobile applications from the same codebase as your web application. Downloading 2.5 MB in an unreliable 3G connection with high latency is a big deal.
Fortunately, we have some techniques to significantly reduce the size of our bundles. We have been using many of them for quite some time-- like minification, concatenation and compression. There are also some newer ones like lazy loading, tree shaking and Ahead of Time (AoT) compilation. To orchestrate these techniques, our tool of choice has been Webpack, mostly because it's easy to use and it's a more mature technology compared to its closest competitor, SystemJS.
The goal for this post is to show you how to create a build system for Angular 2 using Webpack, while applying most of the optimization techniques listed above. The reason to apply most and not all the techniques is due to an incompatibility of Typescript versions that the Angular compiler and Webpack 2 expects. The compiler requires Typescript 1.9 while Webpack 2, in order to be able to perform tree shaking, expects Typescript 2.x. This issue might get fixed in the next release candidate of Angular 2 (RC6) but for now, we are going to focus on tree shaking and will leave the integration with the Angular compiler for a future blog post.
Application Structure
Before we start talking about the nuances of Webpack required to create a build system, let's see the folder structure of our "Hello World" application.
.
├── dist
├── src
│ ├── app
│ │ ├── app.module.ts
│ │ └── root
│ │ ├── index.ts
│ │ ├── root.component.css
│ │ ├── root.component.html
│ │ └── root.component.ts
│ ├── index.html
│ └── main.ts
├── package.json
├── tsconfig.json
└── typings.json
└── webpack.config.js
In this example, we have two folders:
- src, where all of our Typescript code lives, and
- dist, where the result of our build system will be deployed.
Inside the src folder we have defined a module with a root component which in turn has a template and styles.
We are going to focus our attention on the configuration files package.json, tsconfig.json and webpack.config.js.
Project Dependencies
To include Angular 2 in our project, we need to specify the required dependencies for the RC5 release.
package.json
{
...
"dependencies": {
"@angular/common": "2.0.0-rc.5",
"@angular/compiler": "2.0.0-rc.5",
"@angular/core": "2.0.0-rc.5",
"@angular/platform-browser": "2.0.0-rc.5",
"@angular/platform-browser-dynamic": "2.0.0-rc.5",
"reflect-metadata": "0.1.3",
"rxjs": "5.0.0-beta.6",
"zone.js": "0.6.12"
}
}
Those are the libraries that our simple application might need at some point, and two of those libraries are not directly called by our code but need to be globally available in the browser. Those dependencies are zone.js and reflect-metadata. It's important to keep that in mind when we start talking about the chunks that Webpack is going to create.
Next, we are going to focus on the development dependencies of our project:
{
...
"devDependencies": {
...
"awesome-typescript-loader": "2.2.1",
"typescript": "2.0.2",
"webpack": "2.1.0-beta.21",
"webpack-dev-server": "2.1.0-beta.0"
}
...
}
Notice that we are using the 2.x branch for all the libraries shown above. This is required in order to be able to perform tree shaking as we are going to see next.
Why Typescript 2 is Needed
Webpack 2 is able to understand the import syntax without the use of loaders and that's why it is capable of performing tree shaking. But that's pretty much the only thing from ES2015 Webpack 2 is able to understand, for any other new syntax we do need the help of a transpiler, in this case Typescript.
What is Tree Shaking? Tree shaking is the ability to remove any code that we are not actually using in our application from the final bundle. It's one of the most effective techniques to reduce the footprint of an application.
Tree shaking is one of the last things that is performed by our build system and this happens after Typescript has finished transpiling our code. The problem is that with Typescript 1.x it was not possible to preserve the import syntax after doing the transpilation to ES5. Luckily, Typescript 2 solves that and it's now possible to have a configuration file as follows:
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
...
},
...
}
The first two lines of the compilerOptions are key for tree shaking. We are telling Typescript to transpile our code to ES5 ("target": "es5") while preserving the import keyword for our modules ("module": "es2015"). This combination of values was not allowed in Typescript 1.x.
Configuring Webpack
The Webpack configuration is pretty standard. We are using the loader awesome-typescript-loader to perform the transpilation and applying the UglifyJsPlugin when creating a production bundle. Below, we are showing just the relevant parts of the configuration file.
webpack.config.js
...
const basePlugins = [
...
];
const prodPlugins = [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
})
];
const plugins = basePlugins
.concat((process.env.NODE_ENV === 'production') ? prodPlugins: []);
module.exports = {
entry: {
globals: [
'zone.js',
'reflect-metadata'
],
app: './src/main.ts',
},
...
module: {
loaders: [
{ test: /.ts$/, loader: 'awesome-typescript-loader' },
...
]
},
plugins: plugins
};
There are three things to keep in mind for the tree shaker to work:
- We need to use version 2.x of the package awesome-typescript-loader.
- We need to keep our application and library code (Angular 2) in the same chunk.
- We need to minify the files.
By default, Webpack 2 is going to perform tree shaking by not exporting the modules that are not needed by our application in the bundle file. But one thing is to not export, for example, a function, and another is to actually remove that function from the bundle. Webpack will be in charge of removing any reference of unused code in our bundle file, but is not going to remove the unused code from the bundle. It is the minifier, in this case the UglifyJsPlugin, that is smart enough to remove code (variables, functions, classes, etc.) that is not actually being used inside the bundle.
In short, when performing tree shaking, Webpack removes the links and UglifyJsPlugin removes the code.
The Results
It's time to see the results of applying tree shaking to our bundles. We are going to perform the build in development mode first to have a file size reference when tree shaking is not performed.
$ npm run build
...
Asset Size Chunks Chunk Names
app.00ca813ccc0897e68928.js 1.99 MB 0 [emitted] app
globals.00ca813ccc0897e68928.js 676 kB 1 [emitted] globals
index.html 318 bytes [emitted]
...
Our build system is creating two chunks, app and globals. The app chunk contains the code of our application and Angular 2 itself, while globals chunk contains the Javascript files that we need to have globally available: zone.js and reflect-metadata. Because our application does not explicitly depend upon those two packages, the global chunk can't be optimized by the tree shaker, it can only be optimized by the minifier.
Let's perform now a production build that applies tree shaking and minification:
$ npm run build:prod
...
Asset Size Chunks Chunk Names
app.b2aebbcbc8b0f3ad76e0.js 767 kB 0 [emitted] app
globals.b2aebbcbc8b0f3ad76e0.js 322 kB 1 [emitted] globals
index.html 318 bytes [emitted]
...
While both chunks decreased substantially in size, we can see that the app chunk is the one that shrank the most. It is now around 30% its original size!
Conclusion
Having an application with a footprint of almost 1 MB is not yet good enough. There's nothing much we can do about the globals chunk, but for the app chunk we could potentially apply the AoT compiler to obtain even better results. We can save that topic for another blog post. For now, when using Webpack as a build system, tree shaking is a good mechanism for reducing the size of an Angular 2 application.