Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better PurgeCSS support #1822

Closed
pera opened this issue Oct 2, 2019 · 23 comments
Closed

Better PurgeCSS support #1822

pera opened this issue Oct 2, 2019 · 23 comments

Comments

@pera
Copy link

pera commented Oct 2, 2019

Description

Following the discussion in #1086 (comment), the Tailwind CSS documentation has a nice page about PurgeCSS, particularly on how class names should be presented in the processed code: https://tailwindcss.com/docs/controlling-file-size/#writing-purgeable-html

Their advice is simply to not dynamically construct class names, since PurgeCSS doesn't perform any sort of code evaluation (it just matches strings that may look like classes in one's code). Buefy has just a few cases like this (e.g. navbar), so to make it work out of the box Buefy should replace these cases to use complete string literals for class names. Another option could be to document the required whitelist; for instance, this is what I am using right now on a vue-cli project:

const PurgecssPlugin = require('purgecss-webpack-plugin');
const glob = require('glob-all');
const path = require('path');

module.exports = {
  configureWebpack: {
    plugins: [
      new PurgecssPlugin({
        paths: glob.sync([
          path.join(__dirname, './node_modules/buefy/**/*.@(vue|js)'),
          path.join(__dirname, './public/index.html'),
          path.join(__dirname, './src/**/*.@(vue|js)')
        ]),
        whitelistPatterns: [/^navbar-/, /^has-text-/, /^fa-/, /^has-numberinput-/]
      })
    ]
  }
}

Why Buefy need this feature

PurgeCSS can greatly reduce the size of production builds, reducing loading times and therefore improving the overall user experience. PurgeCSS is also the only tool of its kind that easily integrates with Vue projects thanks to purgecss-webpack-plugin.

@l2aelba
Copy link

l2aelba commented Oct 3, 2019

Yes please

@jtommy
Copy link
Member

jtommy commented Oct 6, 2019

Surely we might improve navbar and other components but in the meantime a doc page is a good idea
@pera would you like to make a PR about it ?

@pera
Copy link
Author

pera commented Oct 6, 2019

@jtommy I will try to have something to push sometime during this upcoming week :)

@Mayurifag
Copy link

Thats going to be cool, hoping to see this change soon! 👍

@richeklein
Copy link

I've been experimenting with the whitelisting feature of PurgeCSS and it seems to be working as long you test very carefully. Here's a good summary post on whitelisting in PurgeCSS.

Below is an example of some of the Buefy components that required whitelisting for one of my projects. I'm only testing this in development and wouldn't be very comfortable using this on a large project that is already in production.

 whitelistPatterns: [/mdi/, /icon/, /is-grouped/],
 whitelistPatternsChildren: [/select/, /switch/, /modal/, /b-tabs/, /autocomplete/, /dropdown/]

@DeBelserArne
Copy link

Also looking forward to this.

Noticed that my tables & other components weren't loading when I ran 'npm run prod', eventually found out that the purgeCss module kicks in and removes all of the CSS

@Liwoj
Copy link

Liwoj commented Jan 3, 2020

@pera Correct me if I'm wrong (still too much to learn in modern web dev) but does the setup posted by you in issue description remove any unused Buefy/Bulma CSS ? Lets say i'm using only a few components and want to purge anything else. It doesn't right ? (all Bulma css is included no matter how many components i'm using)

@richeklein
Copy link

@Liwoj If you decide to make a custom build of Bulma/Buefy, you can probably eliminate a good portion of the unused CSS. PurgeCSS does this automatically by analyzing the CSS used in your files to reduce the final size of your production CSS. However, to my knowledge, PurgeCSS is not able to pick up CSS applied dynamically inside Vue components, so you have to manually whitelist some of the CSS rules used inside Buefy components. If you decide to go this route, it's probably easier to do this at the start of your project and whitelist as you develop. I tried running PurgeCSS later on in a project and it was hard to find which classes to whitelist as I had to manually check every page.

@Liwoj
Copy link

Liwoj commented Jan 3, 2020

PurgeCSS does this automatically by analyzing the CSS used in your files to reduce the final size of your production CSS

But if you pass all Buefy components into Purgecss plugin as in the description, it will be as you are using all components and remove nothing. Correct ?

@richeklein
Copy link

But if you pass all Buefy components into Purgecss plugin as in the description, it will be as you are using all components and remove nothing. Correct ?

I think PurgeCSS searches within your HTML to look for CSS rules that are actually being used in your front-end. It then removes what it believes to be the unused CSS rules before your final CSS bundle is created. It works extremely well, but struggles with dynamically generated CSS rules within Vue components since the CSS rules don't actually exist in the HTML. You have to whitelist the CSS rules.

@wrabit
Copy link
Contributor

wrabit commented Apr 1, 2020

@neovive did you get to a point where you are using PurgeCss in production?

@richeklein
Copy link

@wrabit I was close, but opted not to use it since I continued finding classes that needed whitelisting with no way to automate the process. I might try PurgeCSS again when starting a new project, but it was very hard to integrate later on in a large project.

@stale
Copy link

stale bot commented Sep 28, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Sep 28, 2020
@stale stale bot closed this as completed Oct 6, 2020
@frellan
Copy link

frellan commented Oct 26, 2020

This is worth looking into again. I do not currently have time to dig into this but might in the future. When I try purgeCSS and manually add back some rules I still get a HUGE performance boost.

It should be possible to provide better support using something like this, right? https://medium.com/@collinslagat/hack-dealing-with-purgecss-deleting-css-classes-from-vue-component-libraries-element-ui-vuetify-955bbe3f8add

@jtommy
Copy link
Member

jtommy commented Oct 27, 2020

It's a good approach, I didn't know it.
Better support? What do you mean?

@Tragio
Copy link

Tragio commented Oct 27, 2020

@jtommy

Everything was preserved — except two things. The fonts and animations. The San-serif fonts that came with Buefy and Element UI were removed along with some animations in Element UI such as the dropdown animations. I haven’t yet figured out how to preserve them.

Aside from the font issue, there is one more issue: conditionally applied classes will not be detected. A good example to illustrate this, we shall use the Dropdown and Select components of Element UI.

Both use the el-dropdown__caret-button class in order to display the caret symbol. However, the Select Component conditionally applies it.

Due to the way the components were written, PurgeCSS finds it difficult to identify the CSS classes hence cannot preserve them.

Another pitfall you may need to watch out for is not adding a dependency of a dependency in your paths option. For example, a component X you have imported and whose path you have added to PurgeCSS may have also imported and used another component Y. You may not know that it is doing this hence you may find that PurgeCSS is still removing important classes. You may need to check which components X is using and also add those to the paths option in PurgeCSS.

From what we have seen, it is possible for Vue Component Libraries to work with PurgeCSS. However, your mileage may vary depending on how the component library has been written. It is up to the developer to do his/her due diligence to see which pitfalls to watch out for.

@phatj
Copy link

phatj commented Nov 28, 2020

I actually wrote something to address this a while back and forgot about it, haha. The article that @frellan linked is quite similar to my approach, but the extra spice is that it adds to paths to PurgeCSS based on whatever Webpack has loaded up (i.e., imports/requires). I end up having to manually whitelist some helper, but most of the time, I don't get any issues from the following Webpack plugin.

import { join } from 'path';
import logger from './logger';

class BuefyPurgeCSSPlugin {
  constructor() {
    const buefyFolder = join('node_modules', 'buefy');

    this.pluginName = 'BuefyPurgeCSSPlugin';
    this.tapPluginName = 'PurgeCSS';
    this.pattern = new RegExp(`${buefyFolder}.*?(?:js|vue)`);

    logger.debug(`Buefy path pattern resolved to ${this.pattern}`);
  }

  apply(compiler) {
    compiler.hooks.compilation.tap(this.pluginName, (compilation) => {
      const plugin = compilation.options.plugins.find(
        ({ purgedStats, options }) => purgedStats && options.whitelist
      );

      // intercept and overwrite the additionalAssets hook
      compilation.hooks.additionalAssets.intercept({
        register: (tapInfo) => {
          if (tapInfo.name === this.tapPluginName) {
            tapInfo.fn = async () => {
              await plugin.runPluginHook(compilation, plugin.options.paths);
            };

            logger.debug(
              `Overwrote ${this.tapPluginName} function...`,
              plugin.options.paths
            );
          }

          return tapInfo;
        },
      });

      compilation.hooks.additionalAssets.tap(this.pluginName, () => {
        if (!plugin) {
          return;
        }

        // Find buefy files loaded from node_modules and add them to plugin paths
        const buefyDependencies = Array.from(
          compilation.fileDependencies
        ).filter((f) => f.match(this.pattern));
        if (buefyDependencies.length) {
          plugin.options.paths = buefyDependencies.concat(plugin.options.paths);
          logger.debug(
            `${buefyDependencies.length} buefy dependencies found...`,
            plugin.options.paths
          );
        }
      });
    });
  }
}

It's been quite a while since I wrote this code, and I'm trying to jog my memory, but these two files shed a bit of light on how PurgeCSS does its thing:

I'll have to attach a debugger on it and wade through it again, but PurgeCSS doesn't see the paths we want it to (as the article says). There is probably an easier way to do it, and I'll be investigating it this week.

One of the big caveats is that when we install/configure buefy with Vue.use(Buefy), it throws all components into Webpack, which sort of (although not completely!) defeats our purpose here.

In testing (before gzip, with all components loaded):

  • unpurged CSS: 320KB;
  • fully purged CSS: 18KB (lul);
  • hackily purged CSS: 216KB

My next step is to programmatically add components based on a scan of directories and some more hacky code to massage buefy tags out.

As an aside, I was going to call this module buefylo (like buffalo—buefy loader), until I found out that the way I'm pronouncing it was wrong, haha.

@jtommy, @Tragio—let me know your thoughts.

@phatj
Copy link

phatj commented Nov 30, 2020

I was able to recover my notes and going through the debugger helped a ton. The basic idea is that we want to use the dependency graph from Webpack in order to inform us which components were loaded.

tl;dr: https://github.com/phatj/nuxt-buefy-loader-example is a working example (generated from yarn nuxt-create, but I've also added some common scaffolding stuff like SASS and style-resources).

It seems like this solution works in both server and static mode. It's a trivial example, so most styles are thrown out, but it's 1/10th of the full size.

image

These two resources are immensely helpful:

The snippet I dropped above does not use the correct compilation point (compilation.fileDependencies seems to just be file dependencies before tree shaking). I've solved this by looking at normal and concatenated modules in all chunks emitted by Webpack.

Some interesting observations:

  • The ESM generated code from buefy tends to load most of the application when asking for ConfigProgrammatic; I've solved for this by just grabbing the buefy/src/... instead (requires a transpile pass in Nuxt).
  • I'm currently getting two sets of autoloaded outputs (one for the client and one for the server)—from what I can tell, it doesn't harm anything as the asset is generated for the client; it's still a bit odd, though.
  • The PurgeCSSDependencyAutoloaderPlugin (please suggest a better name, lol) is intentionally agnostic to Buefy as other component libraries can likely reuse it.

In terms of the next steps, I think that the AutoloaderPlugin and the loader should be two different packages:

  • one for the Webpack plugin;
  • and the loader can be merged to nuxt-buefy (we make full use of ejs templating to help tree shake dependencies).

This project could probably benefit from investigating the ESM generator weirdness (it's probably heavily related to import * as components) and perhaps improving the config interface as well as some side effects.

I'm excited to slide this in some of my production apps and see what happens.

Cheers!

@phatj
Copy link

phatj commented Nov 30, 2020

Just a quick update on this journey: I've fixed some resolution logic in the AutoloaderPlugin which show now work in both nuxt server and static modes.

I've tossed it into a production environment, and she's humming along (~100KB lighter than usual). My matching logic was trying to be too fancy; I went old school with dual loops and an early bailout condition. It's much easier to read now.

I'll probably sit down and shift these into separate packages/pull requests eventually, but it requires more thought on supporting non-nuxt environments. In the meantime, for anyone needing an immediate fix, you can copy the buefy-loader from the above sample repo.

@Tragio
Copy link

Tragio commented Dec 2, 2020

@phatj you're amazing. Really thank you for your fantastic work!! Really curious to see the evolution and working on Gridsome. Please keep us informed of your work and as soon as you have a little tutorial, repository, or something else we can share and find more people to test it 😄 .

@ryansosin
Copy link

@phatj - thank you for putting this together!

@Tragio
Copy link

Tragio commented Jan 31, 2021

@phatj curious to know the progress in the last months? Have it worked perfectly? Thank you for all the work.

@musikid
Copy link

musikid commented Aug 12, 2021

From the work of @phatj, I created a module, if anyone would need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests