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.
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.
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.
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:
- 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:
{
"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. 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);
}
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. 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.
Top comments (16)
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 👍
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.
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.htmlSo we need to use specific extractors, like so :
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...
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 :)
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
When i run script => it say that "MODULE_NOT_FOUND".
I miss any thing right here? I have install
purgecss
: '^4.0.3'I think, it happens because the PATH issue.
Thanks a lot, man! Awesome aticle!
With angular universal just replace dist by dist/client/browser
How I can add some safelist to exec string
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.
Thanks, but I find better and clear solution.