DEV Community

Dylan v.d Merwe
Dylan v.d Merwe

Posted on

Reduce Angular style size (using PurgeCSS to remove unused styles)

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

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.

Alt Text

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 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

I found this hard to work through in a bigger project, hence why I logged a potential feature request 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 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.

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:

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:

{
  "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",
  },
Enter fullscreen mode Exit fullscreen mode

You can learn about npm scripts here. 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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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' │
└─────────┴───────────────────────────────────┴──────────────┴───────────┘
Enter fullscreen mode Exit fullscreen mode

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. 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' │
└─────────┴───────────────────────────────────┴──────────────┴────────────┘
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (16)

Collapse
 
daviddalbusco profile image
David Dal Busco

Thank your for the share Dylan! Not in an Angular project but, I've got an issue/reminder open since a couple of months about purgeCSS. Now I know where to begin 👍

Collapse
 
swamim profile image
Swaminathan

Many thanks, Dylan! It worked. 👍

In angular material, color="primary" changed to mat-primary in runtime. Post-build (PurgeCss), All the mat classes are removed from styles.css which are not used in any of the HTML/js files.

Lets say

<mat-toolbar color="primary"></mat-toolbar>

post purge css you won't see the primary color on the toolbar. To overcome this instead of color="primary" used the class mat-primary directly

<mat-toolbar class="mat-primary"></mat-toolbar>

Please suggest if any other better way of doing this.

Collapse
 
ruisilva profile image
Rui

This is good but a bit strict.
I made this gist that should be able to read deeper levels of css files and treat those as well.
It can probably can be optimized.
gist.github.com/ruisilva450/a885bf...

Collapse
 
aboudard profile image
Alain Boudard

Hello all, just a quick note about some css frameworks, they often use the 2 dots notation : for handling media queries, and this kind of notation doesn't work out of the box with purgecss, as stated here : purgecss.com/extractors.html

So we need to use specific extractors, like so :

defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
Enter fullscreen mode Exit fullscreen mode
Collapse
 
zakimohammed profile image
Zaki Mohammed

Wonderful article, I have tried all the steps and it actually worked with significant difference in the file size.

But as you mentioned it's not that straightforward with complicated UI packages. I have tried with Kendo UI for Angular, and the results were not appealing, in some places it removed the actual Kendo CSS those were needed and broken the UI.

I have tried with Purge CSS's safelist solution too but if I add the Keno CSS then the size of the bundle results to be almost same size of the original one.

So, we have to be very sure this work with third party packages properly or not. I am thinking to park this approach for now will do some more experimenting later. Anyways nice read though and very well explained the problem.

Bookmarked and already loved the article, thanks :)

Collapse
 
crebuh profile image
Christofer Huber • Edited

Thanks for the article Dylan. It gave me the right direction to reduce the css bundle size. But it introduce an issue with the update process of the service worker. Due to the fact that the css file is modified post build it causes a hash mismatch.

This leads to the fact that my app wont update in production. To solve this you could add @angular-builders/custom-webpack to the project and create a webpack configuration. This then could be integrated with the angular cli and takes care of the purgecss logic during the angular build.

A good example could be found here: stackoverflow.com/a/59500146/1315263

Collapse
 
technbuzz profile image
Samiullah Khan

I am using Angular Material with tailwind. How does it work with tailwind because it by default used purge for production?

Collapse
 
1antares1 profile image
1antares1

Thanks a lot, man! Awesome aticle!

Collapse
 
fabianlaine profile image
Fabian Lainé

With angular universal just replace dist by dist/client/browser

Collapse
 
funny_t1 profile image
Funny t1

How I can add some safelist to exec string

Collapse
 
anurag_vishwakarma profile image
Anurag Vishwakarma

To add a safelist to an exec string in Angular, you can use the "DomSanitizer" service to sanitize the string and ensure that it only contains safe values.

Collapse
 
funny_t1 profile image
Funny t1

Thanks, but I find better and clear solution.

  1. Create config Image description
  2. Changed purge cli comand string

Image description

Collapse
 
minhphupham profile image
Minh Phú

When i run script => it say that "MODULE_NOT_FOUND".
I miss any thing right here? I have install purgecss: '^4.0.3'

Collapse
 
dhineshvrajendran profile image
Dhinesh Rajendran

I think, it happens because the PATH issue.

Collapse
 
rebaiahmed profile image
Ahmed Rebai

Hi man, thanks for your article does this technique will reduce the bundle size? and you didn't included the prebuild.ts

Collapse
 
tamirazrab profile image
Tamir

Hi, I've tried but I've got the same size as before, it didn't purge any css, does that mean no unused css was found. I've tried with different files but same result.