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

DropCSS as an alternative to PurgeCSS #161

Closed
leeoniya opened this issue Apr 11, 2019 · 8 comments
Closed

DropCSS as an alternative to PurgeCSS #161

leeoniya opened this issue Apr 11, 2019 · 8 comments

Comments

@leeoniya
Copy link

leeoniya commented Apr 11, 2019

Hey @adamwathan,

I wanted to do a shameless plug for my new CSS purging lib. I wrote it after encountering numerous unresolved/unresolvable issues with PurgeCSS.

https://github.com/leeoniya/dropcss

Recent discussions:

Initial alpha:

v1.0.0 milestone (with more prose):

Feel free to close this issue without comment, too - no feelings will be hurt.

cheers and thanks!
Leon

@leeoniya
Copy link
Author

leeoniya commented Apr 11, 2019

i ran a quick test against PurgeCSS using the html source of https://next.tailwindcss.com/ and main.css + docsearch.min.css.

[ 'DropCSS', '82ms', '7.79 KB' ]
[ 'PurgeCSS', '342ms', '12.11 KB' ]

i've attached the pretty-printed results for diffing: output.zip

the big caveat here is that due to Tailwind's "interesting" - but technically valid - conventions, DropCSS removes some stuff it shouldn't:

  • .xl\:w-1\/5
  • <div class="lg:w-1/4 xl:w-1/5 pl-6 pr-6 lg:pr-8">

i'll test what perf impact handling these super-odd cases has and get back to you. out of curiosity, what's the history behind these choices rather than using selectors that don't require escaping?

@leeoniya
Copy link
Author

leeoniya commented Apr 11, 2019

i got a much easier/faster pre/post-processing solution working. i temporarily re-map the escaped sequences in both the html and css to strings that match /[\w-]/. this forces the classnames to appear contiguous to DropCSS's tokenizer and only adds ~3ms, plus doesnt require any changes to DropCSS's core:

// remap
let css2 = css
    .replace(/\\\:/gm, '__0')
    .replace(/\\\//gm, '__1')
    .replace(/\\\?/gm, '__2')
    .replace(/\\\(/gm, '__3')
    .replace(/\\\)/gm, '__4');

let html2 = html.replace(/class=["'][^"']*["']/gm, m =>
    m
    .replace(/\:/gm, '__0')
    .replace(/\//gm, '__1')
    .replace(/\?/gm, '__2')
    .replace(/\(/gm, '__3')
    .replace(/\)/gm, '__4')
);

res = dropcss({
    css: css2,
    html: html2,
});

// unmap
res.css = res.css
    .replace(/__0/gm, '\\:')
    .replace(/__1/gm, '\\/')
    .replace(/__2/gm, '\\?')
    .replace(/__3/gm, '\\(')
    .replace(/__4/gm, '\\)');

new timings:

[ 'DropCSS', '85ms', '10.57 KB' ]
[ 'PurgeCSS', '336ms', '12.11 KB' ]

if you look at the diff between them now (and manually verify against the html), it's pretty scary:

output.zip

the only drawback to this is that the shouldDrop() hook [1] will see the selectors with the replacements, so this must be accounted for if that hook is to be used to test against a whitelist. it's a small price to pay though.

[1] https://github.com/leeoniya/dropcss#usage--api

@adamwathan
Copy link
Member

out of curiosity, what's the history behind these choices rather than using selectors that don't require escaping?

Honestly just because it's nicer to read w-1/2 than w-one-half or w-1_2, and because using a : for prefixes makes them feel like labels or object keys which is a good mental model for how Tailwind's utility variants work.

You mention in your Reddit write-up that this project parses HTML — that's honestly the biggest worry for me because the whole reason Purgecss is great is that it doesn't parse HTML, it just extracts a list of tokens from your content file using a simple regex. That makes the mental model for Purgecss really simple — if you want to make sure a class doesn't get accidentally purged, just make sure it appears as a complete string anywhere in your template. All of the purging can be done statically without trying to evaluate every branch of your JavaScript or anything like that.

When you're parsing HTML to find classes, how do you handle properly removing unused classes from Vue or React components that do stuff like this?

<template>
  <div :class="classes"></div>
</template>

<script>
  export default {
    props: ['active'],
    computed: {
      classes() {
        return this.active ? 'bg-blue-100' : 'bg-gray-200'
      }
    },
  }
</script>

@leeoniya
Copy link
Author

leeoniya commented Apr 12, 2019

Good questions.

TL;DR: It's quite easy to attain PurgeCSS's functionality using a small DropCSS wrapper, but impossible to do the inverse.

The philosophies of PurgeCSS and DropCSS are somewhat different. PurgeCSS is designed to operate [imperfectly] on all sorts of sources via extractors, while DropCSS is designed to operate perfectly on the uniform HTML result. You would not be able to run random input like JSX or Vue files through DropCSS, however this is an easily solved situation.

The whole assignment is to build a whitelist to make sure dynamic things missing from the HTML don't get removed. DropCSS provides a shouldDrop(sel) hook that should be used for whitelist testing.

If you wanted to take your Vue file and parse all strings out of it into a whitelist, it's rather trivial:

https://jsfiddle.net/nt6ge0r9/

let tpl = `
    <template>
      <div :class="classes"></div>
    </template>

    <script>
      export default {
        props: ['active'],
        computed: {
          classes() {
            return this.active ? 'bg-blue-100' : 'bg-gray-200'
          }
        },
      }
    <\/script>
`;

let whitelist = [], m;

const STRINGS = /["']([^'"]*)["']/g;

while (m = STRINGS.exec(tpl))
    whitelist.push(m[1]);

let html = '<div class="bg-blue-100">Hello World!</div>';
let css = `
    .bg-blue-100 {color: blue;}
    .bg-gray-200 {color: red;}
`;

let out = dropcss({
    html,
    css,
    shouldDrop: sel =>
        !whitelist.some(keepStr =>
            sel.indexOf(keepStr != -1)
        )
});

console.log(out.css);

With this you've essentially put together your own "extractor".

@adamwathan
Copy link
Member

Yeah cool, so I'd be curious to see what the performance comparison is like if you have to do that anyways. I'd have to do it for every single project I work on because I don't work on vanilla HTML in general. I think the API would have to be a lot more ergonomic for that sort of situation for me to recommend it instead of Purgecss as well. Performance isn't a huge issue because Purgecss is only used in production builds anyways, so if it takes 3s that's really not a problem at all.

@leeoniya
Copy link
Author

leeoniya commented Apr 12, 2019

Yeah cool, so I'd be curious to see what the performance comparison is like if you have to do that anyways.

Of course it depends on your inputs, so i have no way of telling you. all i can say is that the numbers i posted above are just for processing the CSS and HTML on https://next.tailwindcss.com/, so you will save at minimum that much, and very likely a lot more once more stuff is thrown in.

I'd have to do it for every single project I work on because I don't work on vanilla HTML in general.

I mean, reusable functions are a thing.

If you take a look at the difference in the outputs i attached, you'll see that PurgeCSS not only fails to purge a ton of stuff, but also removes things that are very obviously present in the HTML. They end up with the wrong 12.11 KB, while DropCSS ends up with the correct 10.57 KB at 4x the performance. I suspect they'll be chasing obscure extractor bugs for the foreseeable future with their 'simple' regex architecture.

If you're unconvinced, feel free to close this issue. At this point I have no arguments (or time) left :).

Thanks for the discussion, btw!

@adamwathan
Copy link
Member

adamwathan commented Apr 12, 2019 via email

@leeoniya
Copy link
Author

no problem :)

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

No branches or pull requests

2 participants