Sitemap

Decrease Angular2 bundle size for production

7 min readSep 17, 2023

--

*The approach in this article has been tested with Angular 9 and 10. The github sample project of this article is https://github.com/osotnikov/angular-s3-compressed.

Introduction

Naturally you want to minimize the size of your distribution when deploying to production.

The steps to do that in the case of angular are: bundling files (i.e. combining them together), minimizing excess whitespace, removing comments and dead code, rewriting code to use short, cryptic names (“minification”) and last but not least compressing the files.

Luckily, as long as you provide the — prod flag when executing the build (i.e. ng build --prod), latest versions of angular take care of all of these actions except for the compression (see https://angular.io/guide/workspace-config#alternate-build-configurations).

This article will present a simple way to achieve that last part with the help of a custom angular builder @angular-builders/custom-webpack. After having compressed files in our dist folder we will be ready to deploy them to our web server of choice.

Notes regarding back end:

In my case I chose to deploy mine to an AWS S3 bucket with public read access and a WebsiteConfiguration. You can of course chose whatever back end solution you wish, the only thing you'll have to take care of yourself is to configure the proper Content-Encoding: gzip headers for all of your responses that fetch compressed resources.

Create project

First execute:

ng new angular-s3-compressed

It’s a good idea to run ng --version first to make sure yours is something above 8 (had bad luck with that one personally but you may be able to make it work, in theory this approach should work with any version from 6 and above although I wouldn't try it with anything below 8).

Dependencies

Next step, is to add the following dependencies to your devDependencies section of your package.json (I will be using npm in this guide but it’s not a requirement for you):

“@angular-builders/custom-webpack”: “10.0.1”,

“compression-webpack-plugin”: “6.0.2”,

“rename-webpack-plugin”: “2.0.0”,

“npm-run-all”: “4.1.5”

… or run npm add “@angular-builders/custom-webpack” “compression-webpack-plugin” “rename-webpack-plugin” “npm-run-all” — save-dev to get the latest versions …

Some notes on each one of the dependencies (feel free to skip this part):

@angular-builders/custom-webpack is an angular cli builder. Angular introduced builders in version 6 but they were officially supported and documented in version 8. In latest versions they are the official, least fussy way, to modify the building and bundling tasks i.e. if you want to use custom bundlers like webpack or browserify do it through custom angular cli builders.

compression-webpack-plugin is a webpack plugin. Basically we will use @angular-builders/custom-webpack to modify our build and compression-webpack-plugin to insert the compression task in the chain of tasks webpack performs to populate our dist folder.

rename-webpack-plugin is used to remove the .gz extension from the compressed files inside the dist folder. We need this because they link to one another using their original names.

Get Olegk Sotnikov’s stories in your inbox

Join Medium for free to get updates from this writer.

npm-run-all convenience plugin that will help us to execute sequentially a few scripts in the scripts section of package.json.

Custom webpack configuration

After adding the dependencies and running “npm install" we now have to create a custom webpack configuration for our @angular-builders/custom-webpack builder. Later we will configure angular to use this builder when running ng build with the --prod flag. For now just create a new file named my.webpack.config.js and paste the following code in. The nice thing about this builder is that it will append this configuration to the default angular builder one, instead of replacing it, so you have to specify only the extra stuff you want it to do.

const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const RenameWebpackPlugin = require('rename-webpack-plugin')

module.exports = {
entry: {
},
output: {
path: path.resolve(__dirname, 'dist') // __dirname is an object provided by nodejs, the directory name of the current module.
},
plugins: [
new CompressionPlugin({
test: /\.(js|css|txt)$/, // "test" refers to the regex pattern to match against when this plugin looks for files to compress
deleteOriginalAssets: true, // leave only the compressed files in the dist folder
}),
new RenameWebpackPlugin({
originNameReg: /(.*).gz/, // capture the original name in a regex capturing group
targetName: '$1' // refers to the previously captured capturing group
})
]
};

Now that we got our custom config we have to link to it from our angular.json file. All you have to do is declare your “builder” property for the “build” command to be “@angular-builders/custom-webpack:browser” and point to the my.webpack.config.js file via the “customWebpackConfig” property under “options”.

 "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-s3-compressed": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"outputPath": "dist",
"customWebpackConfig": {
"path": "./my.webpack.config.js"
},
"index": "src/index.html",
"main": "src/main.ts",

[...]
// refer to the full file here : https://github.com/osotnikov/angular-s3-compressed
}
}
}
}
}

Results

In my case running ng build when using the default @angular-devkit/build-angular:browser produced an output of 4.54mb, this is quite big but justifiable since it preserves prettified code and sourcemaps to aid in debugging. By running the same configuration with ng build --prod we get down to just 168KB, pretty good but we can do twice as good. By using @angular-builders/custom-webpack:browser I got mine down to just 72KB.

AWS S3 Backend

Here I will present a very basic backend configuration to serve compressed files as a cloudformation template. This will create the bucket along with the website configuration but that in itself will not be enough. We will also have to apply meta information on each compressed file we upload. I will present a practical way to take care of that too.

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Parameters:
FrontEndBucketName:
Type: String
Default: compressed-website-bucket
Description: The name for your s3 bucket that will host your website's files (static content)

Resources:
FrontEndFilesBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref FrontEndBucketName
AccessControl: PublicRead # Canned ACL on the bucket level, by itself won't allow public read on individual files.
WebsiteConfiguration: # We need to provide this section for the bucket to function as a static website server
IndexDocument: index.html
ErrorDocument: error.html
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: FrontEndFilesBucketPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects # Policy to make all object uploaded to the bucket public
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref FrontEndFilesBucket
- /*
Bucket: !Ref FrontEndFilesBucket
Outputs:
WebsiteURL: # Public URL to access our website on
Value: !GetAtt
- FrontEndFilesBucket
- WebsiteURL
Description: URL for website hosted on S3
S3BucketSecureURL:
Value: !Join
- ''
- - 'https://'
- !GetAtt
- FrontEndFilesBucket
- DomainName
Description: URL/Name of S3 bucket to hold website content

Scripts

Half the job in dealing with the backend are the scripts you have to prepare. Organize them neatly and you’ll save yourself a lot of hassle. We will make use of npm and put them in our package.json.

{
"name": "angular-s3-compressed",
"version": "0.0.0",
"config": {
"awsProfile": "myprofile",
"templateFile": "template.yml",
"packagedTemplateFile": "packaged.yaml",
"region": "us-east-1",
"stackName": "compressed-website-stack",
"stackBucketName": "compressed-website-stack-bucket",
"frontEndBucketName": "compressed-website-front-end-files-bucket"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"setenv": "setx AWS_PROFILE %npm_package_config_awsProfile%",
"whoami": "aws sts get-caller-identity",
"create-infrastructure": "aws cloudformation deploy --template ./%npm_package_config_templateFile% --stack-name %npm_package_config_stackName% --parameter-overrides FrontEndBucketName=%npm_package_config_frontEndBucketName%",
"describe": "aws cloudformation describe-stacks --region %npm_package_config_region% --stack-name %npm_package_config_stackName% >> latest_stack_description.json",
"delete-last-desc": "del latest_stack_description.json",
"clean-create": "run-s setenv create-infrastructure delete-last-desc describe",
"upload-fe": "build_and_deploy.bat",
"delete-stack": "aws cloudformation delete-stack --stack-name %npm_package_config_stackName%",
"delete-files": "aws s3 rm s3://%npm_package_config_frontEndBucketName% --recursive",
"delete-all": "run-s delete-files delete-stack"
},
"private": true,
"dependencies": {
[...]
},
"devDependencies": {
[...]
}
}

“scripts” section contains scripts for a windows platform, you’ll have to modify them slightly to execute them on linux

First notice that the “config” section contains the arguments to our scripts. In order to reference any argument in the “scripts” section you’ll have to prefix it’s name with “npm_package_config_” and put it between %%.

I will also assume that you have aws cli installed and your .aws folder contains valid configurations. If that is not the case it’ll be fairly easy to find guides online to achieve that.

Some notes on the scripts, pay special attention to upload-fe and build_and_deploy.bat script as it not only builds the app and uploads the files to s3 but also attaches metainformation on the compressed files which will tell s3 to serve them along with a Content-Encoding: gzip header which will inform the user’s browser to decompress them. In order to write e.g. npm run setenv.

setenv: Sets the AWS_PROFILE environment variable so the next aws cli commands execute against your selected aws profile.

whoami: Convenience command to check that you’re pointing to the correct aws profile.

create-infrastructure: Will create the stack described in template.yml.

describe: Will create a stack description (listing its outputs) in a file named latest_stack_description.json.

upload-fe: Will build the application and upload all the files in the dist folder to the s3 bucket. These are the contents of build_and_deploy.bat:

@echo off
echo *********** Set AWS_PROFILE to stockmonitor *************
set AWS_PROFILE=myprofile
echo. & echo *********** Build UI ************* & echo.
call ng build --prod
echo. & echo *********** Upload front end to s3 ************* & echo.
aws s3 sync --exclude "index.html" --exclude "favicon.ico" --metadata-directive REPLACE --content-encoding "gzip" dist s3://%npm_package_config_frontEndBucketName%
aws s3 sync --exclude "*" --include "index.html" --include "favicon.ico" dist s3://%npm_package_config_frontEndBucketName%

As you can see we use the “aws s3 sync” command twice, first to upload the compressed files and then the rest ones. When uploading the compressed files we attach metainformation on them which will make s3 serve them with a Content-Encoding:gzip header, that is all that is needed to serve compressed files, user’s browser will take care of the rest!

delete-all: First executes delete-files and then delete-stack, the reason being that aws won’t allow you to delete an s3 bucket if it contains files.

If you’re wondering about run-s it’s just a CLI command to run given npm-scripts sequentially. It’s quite convenient given that doing that in windows is not super straightforward and I guess it make the scripts bit more cross platform.

--

--