How Tree Shaking Works in ASP.NET Core Angular SPA

If you've been using ES2015 modules throughout your javascript codebase; in other words: import(ing) and export(ing) modules, there is a good news for you. You can eliminate unused modules (dead code) from your published script when using different module bundlers available online. And this process of dead code elimination is known as so called Tree Shaking. While rollup (a trending module bundler) being the first to introduce the concept and implement it, other module bundlers came to realize that this is a must have feature and soon they also start implementing the feature in their own way. webpack is a popular module bundler, that is extensively used in Angular application development. In this post, I'll show you how tree shaking is done using webpack in the ASP.NET Core Angular SPA template.

First, let's see how tree shaking actually works with a simple demo. Fire up a ASP.NET Core Angular SPA application. The following commands will install all the available spa templates from dotnet and initialize an Angular SPA application in a directory you are currently in:

dotnet new -i "Microsoft.AspNetCore.SpaTemplates::*"
dotnet new angular

The reason I'm using this specific template is because everything is already been setup/installed to apply tree shaking.

Once initialized, open the project in Visual Studio or VS Code. I'm using VS Code here. Open the webpack.config.js file and comment out everything (we will come back later) and paste the following lines of code:

const path = require('path');
const webpack = require('webpack');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;

module.exports = {
    entry: './treeshaking/main-module.ts',
    output: {
        filename: 'main-module.js',
        path: path.join(__dirname, './wwwroot')
    },
    resolve: { extensions: [ '.ts' ] },
    module: {
        rules: [
            { test: /\.ts$/, use: ['awesome-typescript-loader'] }
        ]
    },
    plugins: [
        new CheckerPlugin()
    ]
};

Not too shabby! The piece of code above will read the main-module.ts file under the treeshaking folder, transpile the content into javascript and then push the script into the main-module.js file under wwwroot folder. Other than this, nothing so special going on here. For transpiling typescript into javascript, we have used a webpack plugin called awesome-typescript-loader (npm install --save awesome-typescript-loader). We are telling webpack to resolve typescript file by specifying .ts in the extensions array. The CheckerPlugin in the plugins array is optional. It is only added to do some async error reporting.

Now, open your command prompt, go to your project directory and run the following command:

webpack

Similarly you can also use the VS Code console like for that:

As you can see, webpack has done all the heavy lifting behind the scene and gave you the transpiled script as main-module.js. If you open the file, you will see something like the following,

main-module.js

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = SayHi;
/* unused harmony export SayBye */
function SayHi() {
    console.log("Hi!");
}
function SayBye() {
    console.log("Bye!");
}

The SayBye, SayHi both are export(ed) functions and can be found in the reference-modules.ts file:

reference-modules.ts

export function SayHi() {
    console.log("Hi!");
}
export function SayBye() {
    console.log("Bye!");
}

Notice, that they are both import(ed) in the main-module.ts file like the following:

main-module.ts

import { SayHi, SayBye } from './reference-modules';

SayHi();

That is why transpiling main-module.ts file has also trnaspiled the imported functions into the main-module.js.

Okay! That was expected. But let's revisit the main-module.js and see what's so special. If you noticed in the earlier code block (main-module.js), you may noticed that we have two lines of comment on top of the functions,

/* harmony export (immutable) */ __webpack_exports__["a"] = SayHi;
/* unused harmony export SayBye */

As you can see, webpack is intelligent enough to tell that, although you have imported both of the functions in main-module.ts, you have only used the SayHi method in it, i.e. SayHi();. It has also specified that you have a unused harmony export SayBye. Harmony is the code name for ES2015/ES6 module system.

That's cool. Now that we know which modules are being used and which are unused, we can remove the unused modules (dead code) from our published script if we want. So, webpack has another plugin called UglifyJsPlugin which is built into it (no need for separate npm install). Just specify the plugin in the plugins array and you are done. Your plugins array should now look like the following:

plugins: [
    new CheckerPlugin(),
    new webpack.optimize.UglifyJsPlugin()
]

Now, if you run the webpack command again, you will see that while doing the uglification, it has removed the exported SayBye function from your published main-module.js file.

You can search for it though, but you will not find it anywhere the main-module.js. And that's the magic of tree shaking.

Now that, we have our basics done, let's see how tree shaking is done in the actual Angular SPA template. If you uncomment the previous code in webpack.config.js file and fiddle around a bit, you can see that tree shaking is done only when you are in production mode.

plugins: [
    new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require('./wwwroot/dist/vendor-manifest.json')
    })
].concat(isDevBuild ? [
// Plugins that apply in development builds only
    new webpack.SourceMapDevToolPlugin({
        filename: '[file].map', // Remove this line if you prefer inline source maps
        moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
    })
] : [
// Plugins that apply in production builds only
    new webpack.optimize.UglifyJsPlugin(),
    new AotPlugin({
        tsConfigPath: './tsconfig.json',
        entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'),
    exclude: ['./**/*.server.ts']
    })
])

Here, isDevBuild decides whether you are in production or development mode. The reason of doing tree shaking only in the production mode is because, eliminating dead code won't do any good in the development mode and also unnecessary.

Now, if you run just webpack command, you will see that the size of your main-client.js is initially around 76Kb. While running the same command with production flag (i.e. --env.prod) will give you a file size that is around 36kb (of course, these statistics will vary depending on your application code):

webpack --env.prod

dev mode:

prod mode:

So, that is tree shaking, used to minimize the size of your published script by eliminating unused (dead) code.

You can further minimize the size of your script by doing AOT (Ahead of Time compilation) in your Angular SPA application. But that will only work if you are in Angular context. That is a future blog topic.

On the other hand, tree shaking can be applied anywhere, whether you are building apps using Angular, React, Vue, or just plain javascript using ES2015 module syntax i.e import and export.

Demo Code Repository: https://github.com/fiyazbinhasan/Aot-TreeShaking