Reduce Angular style size (using PurgeCSS to remove unused styles)
Tags: angular, css, webdev
Published: 2021-01-20 21:23:13

We want to keep our bundle sizes as low as possible right? I have been investigating how to do this, specifically around the CSS files output by Angular projects. 

**Wait, doesn't Angular do this already?** Not really. Angular will put styles from your components directly into the .js files, and all third-party, library or global styles go into a dedicated styles.css in the `/dist` folder. It won't remove any unused styles automatically. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gu4bcs772hl1g9r8ooy7.png)

I want to investigate some other options, and then show you the solution I ended up implementing on some of our bigger projects to show you the savings. 

#What options are there?#

### VSCode plugin
We can use some plugins in our IDE to help identify styles that are not used. 

For example there is [Unused CSS Clases for JavaScript/Angular/React](https://marketplace.visualstudio.com/items?itemName=rcore.rcore-unused-css-classes). 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d5iytgrt4vstiapv2z78.png)

Although very useful while building pages and components it does have some drawbacks with Angular and SASS such as not correctly working with SASS mixins, understanding class bindings `[class.highlighted]="highlight"`, or working out what is going on inside of `::ng-deep`. 

I do recommend doing development with this plugin installed so that you can easily pick up in your style sheets potential unused style classes that you can clean up as you go.

### ngx-unused-css
There is an npm package called [ngx-unused-css](https://github.com/ivanblazevic/ngx-unused-css) that, when installed and run on your project, will scan your files and provide a list of all styles it deems are not used.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ykmv0xf130mv49w0ep9h.png)

I found this hard to work through in a bigger project, hence why I logged a potential [feature request](https://github.com/ivanblazevic/ngx-unused-css/issues/17) to help. Probably more useful for smaller projects that do not have many components/pages. 

**Which brings us to the solution I ended up implementing...**

### PurgeCSS

[PurceCSS](https://purgecss.com/) is a well-known tool that scans the output of the built CSS files and will use it's heuristics and extractors to remove unused CSS - predominantly brought to fame thanks to [Tailwind](https://tailwindcss.com/).

When working in big projects, often with multiple team members or contributors, it is hard to keep track over time when styles are no longer in use. This bloats the CSS that is shipped to the browser which, while not as dramatic as large JavaScript files, does contribute towards higher data use and CPU time to parse. 

Getting PurgeCSS to work alongside Angular and the Angular CLI is quite a challenge for various reasons. I investigated numerous ways:
 - https://stackoverflow.com/questions/58112925/how-to-integrate-purgecss-with-angular-cli-project
 - https://medium.com/@joao.a.edmundo/angular-cli-tailwindcss-purgecss-2853ef422c02
 - https://github.com/FullHuman/purgecss/issues/96

If these work for you that is awesome. But I needed something that would fit into my project workflow and not be as intrusive.

**So I wrote my own npm postbuild script**

In my `package.json`, I have the following scripts:

```json
{
  "name": "test-app",
  "version": "1.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "lint": "tslint src/**/*.ts --config tslint.json --project tsconfig.json ",
    "lint:fix": "tslint src/**/*.ts --config tslint.json --fix --project tsconfig.json",
    "prebuild": "node environments/prebuild.ts",
    "postbuild": "node environments/postbuild.js",
    "update-angular": "ng update @angular/cli @angular/core --allow-dirty",
  },
```

You can learn about npm scripts [here](https://docs.npmjs.com/cli/v6/using-npm/scripts). Note the `postbuild` script - npm  will run this script once the `build` step is complete. The `build` step just runs `ng build` as defined above. The `postbuild` script does _not_ run while we are doing development with `ng serve`.

What I want the `postbuild` script to do is to perform the PurgeCSS step, replace any style files that it has changed and then provide an output of any file size differences. 

In my environment, our tooling runs `npm run build` and `npm run build -- --prod` when deploying to the dev or production environments respectively. Now with the `postbuild` script included, the PurgeCSS step will happen right after the build completes and before the deployment of any files. 

**The postbuild.js script**

_Note that this has been tested on Windows._

```js
const exec = require('child_process').exec;
const fs = require('fs');
const path = require('path');

// find the styles css file
const files = getFilesFromPath('./dist', '.css');
let data = [];

if (!files && files.length <= 0) {
  console.log("cannot find style files to purge");
  return;
}

for (let f of files) {
  // get original file size
  const originalSize = getFilesizeInKiloBytes('./dist/' + f) + "kb";
  var o = { "file": f, "originalSize": originalSize, "newSize": "" };
  data.push(o);
}

console.log("Run PurgeCSS...");

exec("purgecss -css dist/*.css --content dist/index.html dist/*.js -o dist/", function (error, stdout, stderr) {
  console.log("PurgeCSS done");
  console.log();

  for (let d of data) {
    // get new file size
    const newSize = getFilesizeInKiloBytes('./dist/' + d.file) + "kb";
    d.newSize = newSize;
  }

  console.table(data);
});

function getFilesizeInKiloBytes(filename) {
  var stats = fs.statSync(filename);
  var fileSizeInBytes = stats.size / 1024;
  return fileSizeInBytes.toFixed(2);
}

function getFilesFromPath(dir, extension) {
  let files = fs.readdirSync(dir);
  return files.filter(e => path.extname(e).toLowerCase() === extension);
}
```

Make sure to run `npm i -D purgecss` to install the right dependency.

## Let's see some examples ##

All of these projects are using **Angular 11**.

####Medium-size project####

This is the output of a medium sized project that includes Bootstrap and has multiple style sheets for different clients. 

```
Run PurgeCSS...
PurgeCSS done

┌─────────┬───────────────────────────────────┬──────────────┬───────────┐
│ (index) │               file                │ originalSize │  newSize  │
├─────────┼───────────────────────────────────┼──────────────┼───────────┤
│    0    │       'benefitexchange.css'       │  '148.48kb'  │ '36.89kb' │
│    1    │       'creativecounsel.css'       │  '148.46kb'  │ '36.89kb' │
│    2    │        'fibrecompare.css'         │  '148.35kb'  │ '36.77kb' │
│    3    │          'filledgap.css'          │  '148.07kb'  │ '36.68kb' │
│    4    │            'hippo.css'            │  '148.43kb'  │ '36.86kb' │
│    5    │          'klicknet.css'           │  '148.49kb'  │ '36.89kb' │
│    6    │            'mondo.css'            │  '148.44kb'  │ '36.87kb' │
│    7    │           'nedbank.css'           │  '148.63kb'  │ '37.05kb' │
│    8    │         'phonefinder.css'         │  '148.41kb'  │ '36.85kb' │
│    9    │       'realpromotions.css'        │  '148.46kb'  │ '36.88kb' │
│   10    │ 'styles.428c935b7c11a505124a.css' │  '33.96kb'   │ '20.33kb' │
└─────────┴───────────────────────────────────┴──────────────┴───────────┘
```

There are some dramatic savings where huge chunks of unused Bootstrap and other library styles are removed.

####Large enterprise project####

This is the output of a large enterprise project that makes use of [https://github.com/NG-ZORRO/ng-zorro-antd](NG-ZORRO). There are plenty of other dependencies such as `ngx-dropzone`, `lightgallery.js`, `ngx-toastr`, `web-social-share`, etc. 

There were some issues with some components in NG-ZORRO such as the date picker and the steps component not working at run time. These will need to be raised with the project maintainers to hopefully resolve - otherwise you may need to use other libraries. 

```
Run PurgeCSS...
PurgeCSS done

┌─────────┬───────────────────────────────────┬──────────────┬────────────┐
│ (index) │               file                │ originalSize │  newSize   │
├─────────┼───────────────────────────────────┼──────────────┼────────────┤
│    0    │ 'styles.72918fcbd85fee3bb2a8.css' │  '545.01kb'  │ '231.97kb' │
└─────────┴───────────────────────────────────┴──────────────┴────────────┘
```

Again, a huge reduction thanks to unused styles being removed.

## Some caveats ##

Make sure you **test your code**. 

In my experience, the less complicated the project the better this approach will work. Once you start using more complicated UI tools and packages (such as NG-ZORRO or Angular Material) you may run into situations where PurgeCSS is not able to determine styles that are used due to run-time interactions on components and these styles will be stripped, causing some very weird looking sites. 

Also note that solution will **only** work on CSS files, it will not remove any styles that Angular builds into your .js files from your components. _Now that would take this to the next level_ - but I think think would require more integration with Webpack and the Angular CLI.

#Conclusion#

I was struggling to manually identify and remove unused styles in my company's Angular projects in order to 1) keep bloat down and 2) ensure the site is as lean as possible. 

By implementing a `postbuild` npm script that runs PurgeCSS on the build output, we have seen some dramatic reductions in style size. I do recommend cleaning up your own CSS or SASS files as the primary measure. 

Overall the majority of projects worked without any changes, but your mileage may vary.

I would love to know if this solution works for you, what size reductions you are getting or if you have any other thoughts on this.