Clean up unused CSS from external component libraries like PrimeNg
Aart den Braber
Ik ben een freelance dev bij DUO met een passie voor mooie code en onderhoudbare applicaties
TL;DR; check out the Stackblitz-demo.
Working with large CSS files used to be ok, back in the day, but nowadays you can't get away with that. Libraries like Tailwind popularized something called PurgeCSS. PurgeCSS makes sure that if you have written styles which are never used in your HTML, these won't be served to your website's visitors as well. This happens at compile time, so you don't have to bother with carefully selecting the styles you use (even though you should probably clean up anyway).
Especially if you use something like Bootstrap or some other utility-library that is very handy but also quite big, it's important that you don't serve all that unused CSS to your user. It can easily mean the difference between a CSS file of 500kb and a file that gives the same functionality to the user of only 30kb.
Yeah yeah, I know what purging CSS is, but what about the component libraries?
For a recent project at Knab, I implemented PrimeNg into my Angular project, so I'll guide you through that. But you can probably use the same principle with any other library which is setup in a similar way.
The problem with component libraries like PrimeNg is that they require you to serve all of their CSS to your visitor, because if you start using a component, it should, you know, look good ??. But since PrimeNg has around 90 components and I'm using only 10 or so, I don't need all that unused styling.
How to purge PrimeNg
To purge the unused styles from the PrimeNg library, we'll need to know which components we have imported into our project; because if we know that, we can tell PurgeCSS to remove all the css which isn't used by those components.
We could require some file to be maintained where all components that we're using in the project are being kept track off. So we define an array like ['button', 'inputtext', 'steps']. However, that makes the application very hard to maintain, especially because any mistake can only be spotted in your test environment (or even production...).
To do this properly in Angular, we need to know where those components are always defined: let's check the module-files where the components are declared (or, in PrimeNg's case: imported as modules. I'll keep calling those modules 'components' to avoid confusion).
We'll just use a NodeJS script for this and loop over all the module.ts-files (there can be multiple if your project is a bit bigger) to see which components are imported.
Let's use glob for this:
glob.sync('src/**/*module.ts', { nodir: true })
This will return an array of files that match the glob, so something like this: ['app.module.ts', 'some-other.module.ts'].
We should loop over the contents of those files to find which PrimeNg components are imported. We use `fs` for that to read the file, and then check if there is some import in there that retrieves from 'primeng/something'. We then 'catch' the 'something'-part in a capture group which we can access later on when adding 'something' to the `primeNgImports`-Set (using a Set makes sure that there will be no duplicates).
领英推荐
So that's the main part of our solution. We now have a list of all components that are imported, something like ['button', 'inputtext', 'steps'], and it's fully dynamic. So we can add any component we want, and this script will detect it.
Now let's find the template-files with those component names so our PurgeCSS can do its magic.
Finding the template-files
First, we should call `getImportedPrimeNgModules` that we created earlier to get the names of the imported components.
Then, we should find the paths of the files where the templates are stored. In the case of PrimeNg, they are actually stored in multiple folders, but the one we're targeting is (in the case of button) ...\node_modules\primeng\esm2020\button\button.mjs
Now - because you and I, we are professionals - we'll not rely on this exact path, because that might change in the future. Instead, let's use glob again - something like 'node_modules/primeng/**/button.*js' which leaves space for the path to change in the future, as long as we're getting our button.mjs file. And while we're at it - we're not sure if it will remain an 'mjs'-file or just a regular 'js'-file or have a different extension, but as long as it's got 'js' in there, this glob will find it.
Of course the 'button' in that glob will be dynamic. We store the file-paths in an array so it can be used.
Now, we could add real tests, but to keep this article short'n'sweet, let's just throw errors if something goes wrong. This makes sure that if, in some future, the creators of PrimeNg decide to change something, our production website won't be without styling, but instead will throw errors during build time so we can fix it.
The final script
Edit: actually, I found a few minor things that didn't work as planned. You can view the updated version here ? and on Stackblitz.
const glob = require('glob');
const fs = require('fs');
const path = require('path');
function findMatches(regex, str, matches = []) {
? const res = regex.exec(str);
? res && matches.push(res) && findMatches(regex, str, matches);
? return matches;
}
function getImportedPrimeNgModules() {
? // Make sure it's a list with unique items
? const primeNgImports = new Set();
? // The components are always declared in a module, so we just have to look through those
? glob.sync('src/**/*module.ts', { nodir: true }).forEach(file => {
? ? const fileContents = fs.readFileSync(file);
? ? const matches = findMatches(/import .*primeng\/([a-zA-Z]*)/g, fileContents);
? ? for (const match of matches) {
? ? ? primeNgImports.add(match[1]);
? ? }
? });
? return Array.from(primeNgImports);
}
module.exports = {
? /**
? ?* We use PrimeNg in this project, but we also wish to keep the css-filesize as small as possible.
? ?* In order to keep the css size small, we use a purger, see webpack.config.js. However, this purger won't 'know' what css styles PrimeNg is using,
? ?* because those templates are defined in the primeng node_modules.
? ?*/
? getPrimeNgTemplateFiles: function () {
? ? const primeNgModules = getImportedPrimeNgModules();
? ? const componentTemplatePaths = [];
? ? primeNgModules.forEach(pModule => {
? ? ? // Use glob to make this as robust as possible. We are looking for something like /primeng/button/button.mjs
? ? ? const templatePaths = glob.sync(
? ? ? ? path.join('node_modules', 'primeng', '**', pModule, `${pModule}\.*js`),
? ? ? );
? ? ? if (!templatePaths.length) {
? ? ? ? throw new Error(`
? ? ? ? ? ? Something went wrong while finding the PrimeNg node_modules for '${pModule}'.?
? ? ? ? ? ? This probably means that something has changed in the component library, and you'll have to find the files where the templates are defined.
? ? ? ? `);
? ? ? }
? ? ? if (templatePaths.length > 1) {
? ? ? ? console.warn(
? ? ? ? ? `
? ? ? ? ? ? We found multiple PrimeNg modules for '${pModule}', is that right??
? ? ? ? ? ? If it is, you can probably remove this warning, but usually there should only be one template file.
? ? ? ? ? ? If it's not, you should probably change the glob ↑ , because that means unnecessary css is left in the styles.css. We're now including:?
? ? ? ? `,
? ? ? ? ? templatePaths,
? ? ? ? );
? ? ? }
? ? ? // This should only return one templatePath, but to make it more robust, let's assume it could also have found more than one.
? ? ? templatePaths.forEach(templatePath =>
? ? ? ? componentTemplatePaths.push(templatePath),
? ? ? );
? ? });
? ? if (!componentTemplatePaths.length) {
? ? ? throw new Error(`
? ? ? ? ? ? We couldn't find any templates for PrimeNg components.?
? ? ? ? ? ? That can't be right, unless you're not using PrimeNg anymore.?
? ? ? ? ? ? In that case, remove the reference to this function from webpack.config.js
? ? ? ? `);
? ? }
? ? return {
? ? ? paths: componentTemplatePaths,
? ? ? modules: primeNgModules,
? ? };
? },
};
This file can be used with webpack. If you want to install it into your Angular project, take a look at the Stackblitz.
If you found this article useful, let me know! If you have suggestions or think this is the worst idea possible, let me know as well ??
Associate Software Engineer @ Brain Station 23
10 个月I tried following this example but this code tends to check all files with this type of path signature: "node_modules/primeng/**/module_name/module_name.*js" which means it'll detect .mjs files as well. There are .mjs files with the same module name inside the same folder(different sub-folders) and therefore ran into an error.
Software Engineer (FE) at Optimum Partners | NextJs | ReactJs | TypeScript | NodeJs
1 年I found this really helpful, thanks Braber but I couldn't use PurgeCSS because it is removing styles from modules that I would rather like to keep intact, is there a way to tell PurgeCSS to purge only specific css files.
Full Stack Engineer
2 年Hi Aart den Braber your tutorial is exactly what I want, can I use your code for my company. Thanks in advanve
Capo @ [code]capi
2 年Top Aart!