leeoniya / dropcss Goto Github PK
View Code? Open in Web Editor NEWAn exceptionally fast, thorough and tiny unused-CSS cleaner
License: MIT License
An exceptionally fast, thorough and tiny unused-CSS cleaner
License: MIT License
Hey, attributes can have :
in attributes, e.g., svgs such as <image xlink:href="example"/>
, the regex needs to be updated.
My use case is that it's hard to have 1 optimized/cleaned/minimal/critical CSS stylesheet per page. What I want is the super set of the CSS for GET /
, GET /about
, GET /support
, GET /search
. Each page uses Bootstrap/Bulma/Whatever but they all each have their own slight differences.
E.g. The original CSS looks like this:
div.foo { }
div.bar { }
div.baz { }
div.woo { }
Every page has <div class="foo">
. The home page (only) has <div class="bar">
. The /about
page (only) has <div class="baz">
.
So, the ideal outcome should be:
div.foo { }
div.bar { }
div.baz { }
personally, i author static stylesheets as js objects and use https://github.com/cssobj/cssobj to compile them (which handles a basic level of optimizations). since DropCSS already tokenizes the css (besides properties), it would be useful to expose the tokens to allow the build-out of a small post-processor which completes the parsing of props and creates a js object that can be fed into cssobj for final optimizations & compression.
probably the only actionable thing for this lib is to return the css tokens alongside the generated css.
Simple example:
<!DOCTYPE html>
<body>
<p>Hello, world!
</body>
body p { margin: 1em; }
In this case, dropcss should notice that:
p
are body p
.body p
, all of the selectors on body p
are either less specific than p
or more specific than body p
.Therefore, it should just output:
p { margin: 1em; }
One potentially more applicable case of this might be styles which differentiate ul li
and ol li
on a page which only uses ul li
. In this case, we could drop the ul
and just output li
to shrink the size of the CSS. This could also apply to, for example, .page-type .element
, where only one variant of .page-type
is used in the HTML samples given.
HTML:
<div class="items">
<div class="item">
<div class="content"></div>
</div>
<div class="item">
<div class="content"></div>
</div>
</div>
CSS:
.items .item:last-of-type .content {}
Error:
TypeError: Cannot read property 'length' of null
at find (dropcss.cjs.js?693)
at find (dropcss.cjs.js?720)
at eval (dropcss.cjs.js?765)
at Array.some (<anonymous>)
at some (dropcss.cjs.js?765)
at _export_some_ (dropcss.cjs.js?772)
at eval (dropcss.cjs.js?960)
at Array.filter (<anonymous>)
at dropcss (dropcss.cjs.js?947)
at eval (index.js?80)
case 'last-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == tsibs.length - 1;
^
break;
:first-of-type
:last-of-type
:only-of-type
:nth-of-type()
:nth-last-of-type()
:nth-only-of-type()
ping me if you're interested in tackling this before i get to it.
A recent fix (8e27afd) for short hand font properties will throw an error if one is using custom properties.
E.g. this rule causes the regex in said commit to not match.
.foo {
font: var(--font-style) var(--font-weight) 1em/var(--line-height) var(--font-family);
}
Tailwind CSS uses "fun" conventions like this:
.xl\:w-1\/5
<div class="lg:w-1/4 xl:w-1/5 pl-6 pr-6 lg:pr-8">
need to test perf impact and if it can be rolled into the IDENT regex:
// now
/([\w*-]+)/iy
// perhaps
/((?:\\.|[\w*-])+)/iy
stripPseudos
regexes would also need to change, along with stripping the escapes in the caches.
not sure if all of it is worthwhile.
see also: tailwindlabs/tailwindcss.com#161
i never quite liked the naming of result.sels
or that it gets accumulated regardless of whether the user asked for it or not. it also doesnt feel right that whitelist accumulation uses this return style but whitelist testing uses the shouldDrop() callback style. didRetain() which fires for every undropped selector feels more consistent.
this would be a breaking change but to a non-primary API, so i dont think a 2.0 is warranted; hopefully no-one's life depends on dropcss 😱
Hello,
I'm working alone on my music blog (https://soundsirius.digitalpress.blog) and I don't have much time every day. I want to use your software to make my website faster. But here's what I need:
I installed Tailwind CSS which is very nice but I'm not using everything. I want to get rid of the bad css and css complexity and make my website load under 1 second.
PS: I also want to use CleanCSS before.
<div></div>
parses well<div ></div>
is good also<div></div >
fails: Error: html parser stopped here: "</div >
All 3 cases are equally correct according to the spec (https://www.w3.org/TR/REC-xml/#sec-starttags)
I wish it was a synthetic case - but that's what Prettier outputs in some cases, so it would be totally cumbersome trying to override
Seems like there's some trouble parsing BEM selectors, namely the --modifier
bit.
const dropcss = require('dropcss');
let html = `
<html>
<head></head>
<body>
<div class="tab-table">
<div class="tab-table__item">Tab 1</div>
<div class="tab-table__item tab-table__item--new">Tab 2</div>
</div>
</body>
</html>
`;
let css = `
.tab-table {
display: flex;
flex-flow: row nowrap;
position: relative;
}
.tab-table__item {
background: #494545;
border-width: 0 .1rem;
cursor: pointer;
flex: 1 1 20%;
flex-flow: column nowrap;
}
.tab-table__item--new::before {
align-items: center;
color: #fff;
display: flex;
justify-content: center;
}
`;
let cleaned = dropcss({
html,
css,
});
console.log(cleaned);
/*
{ css: '.tab-table{display: flex;\n flex-flow: row nowrap;\n position: relative;}.tab-table__item{background: #494545;\n border-width: 0 .1rem;\n cursor: pointer;\n flex: 1 1 20%;\n flex-flow: column nowrap;}.tab-table__itemcolor: #fff;\n display: flex;\n justify-content: center;}',
sels:
Set {
'.tab-table',
'.tab-table__item',
'.tab-table__item--new::before' } }
*/
Formatted CSS to more clearly see what's going on:
.tab-table {
display: flex;
flex-flow: row nowrap;
position: relative;
}
.tab-table__item {
background: #494545;
border-width: 0 .1rem;
cursor: pointer;
flex: 1 1 20%;
flex-flow: column nowrap;
}
.tab-table__itemcolor: #fff; /* should be `.tab-table__item--new::before {` */
display: flex;
justify-content: center;
}
dropcss version: 1.0.12
node version: 10.16.0
Hi.
Is there any build of dropcss to use in a Gulp task ?
Thanks you
This lib is not working in IE11 and other browsers that don't support m and y regexp flags.
#18 makes possible to polyfill it.
I think that dropcss is an awesome tool for collecting CSS usage at production, directly from the user browser, in requestIdleCallback
or WebWorker. It's small and relatively fast. For projects with the big codebase and a lot of legacy, it's impossible to use it at build time.
DropCSS already contains the vast majority of the machinery necessary to create something akin to these libs:
https://github.com/fb55/css-select
https://github.com/jquery/sizzle
https://github.com/dperini/nwmatcher
the amount of additional code/tweaks should be quite small to get this working and there's a reasonable chance that a selector engine built using DropCSS's innards would be very competative in speed and definitely size. Sizzle is 19.4KB (min) while DropCSS is 9.55KB (min) which includes an HTML parser [1] and post-processor [2] that can mostly be ditched.
[1] https://github.com/leeoniya/dropcss/blob/master/src/html.js
[2] https://github.com/leeoniya/dropcss/blob/master/src/postproc.js
I was wondering if you've considered a separate cli version of dropcss. I was thinking of developing it when I have time, so I can use it to lint on my CI, and exit with an error if there's any unused css. Kind of like how I use prettier on CI. Would you mind if I create dropcss-cli?
Hi,
does this package exist as a postcss plugin ?
thanks
Tried with the latest release. Still getting infinite loops. This time I was testing this page:
document.querySelector('#critical-styles').innerText
I suspect that the issue is again some weird selector that is not being parsed. It would probably be best to add back the bail outs for at least the CSS parser so that we can know when it gets stuck.
Not sure if this was already mentioned somewhere else - have you thought of adding a css selector scanner for .js files? Similar to https://github.com/FullHuman/purgecss-from-js-experimental
To be able to detect dynamically added classes - e.g. from carousel plugins and such.
Hi man I've been trying to compile some css, and what happened is that I accidentally had this:
const html = `
<body>
<li heading="<em>your-signature-here</em>">
</li>
<h4 id="<em>your-signature-here</em>" class="mt-2"><em>Your Signature Here</em>:</h4>
</body>`
although it's probably wrong to put tags in attributes (it was autogenerated), the issue is that the error from dropcss
does not really give much info
TypeError: Cannot read property 'parentNode' of null
at build (/Users/zavr/adc/splendid/node_modules/dropcss/dist/dropcss.cjs.js:154:17)
is it possible to maybe give more debug info?
also maybe tags in attributes should result in some kind of warning? thanks.
Related #16
Here's an example:
https://repl.it/@heygrady/dropcss-example
It would be nice if it would throw instead of infinite looping. It doesn't matter much to me if this throws but infinite loops are a pretty big deal. Is it possible to put some runaway loop protection in place?
Regarding #16 (comment)
https://github.com/leeoniya/dropcss/blob/master/src/html.js#L66-L67
https://github.com/leeoniya/dropcss/blob/master/src/css.js#L159-L160
Would this basically fix it?
let prevPos = pos
while (pos < html.length) {
next();
// is the problem that we stop advancing in some cases?
if (prevPos === pos) {
throw new Error('tokenizer did not advance');
break;
}
prevPos = pos;
}
css:
p:not(:nth-child(n+3)) { color: green }
html:
<div id="root">
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
</div>
ping me if you're interested in tackling this before i get to it.
Don't want to paste in everything here but I ran into an issue where certain pages on my site would go into an infinite loop during the dropcss
call. It's probably some weird css causing the issue but it's impossible to know because it gets stuck in the loop and never errors out.
i've personally not needed it, but it could be a quality of life improvement.
probably via https://github.com/Rich-Harris/magic-string but not sure how useful it would be without also ingesting a prior sourcemap, so also https://github.com/Rich-Harris/sorcery.
performance is somewhat a concern since we now need to also store indices in the token list.
perhaps the better way to keep this optional and out of the core is to optionally return a list of indices alongside the token list with indications of which were removed and leave the actual work of doing the sourcemapping to an external wrapper instead of bloating the core.
dropcss({trackLocs: true})
{
css: ...,
locs: [
655, // index
12, // chars removed
928, // index
533, // chars removed
]
}
or better yet? add the location index as arg to shouldDrop(sel, loc)
and let the consumer/wrapper accumulate the deleted array. (still behind an option flag).
.three {
background-color: var(--my-var, var(--my-background, pink));
--border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white));
}
https://developer.mozilla.org/en-US/docs/Web/CSS/font
@font-face {
font-family: "Open Sans";
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
}
body {
font: italic small-caps normal 13px/150% Arial, 'Open Sans', Helvetica, sans-serif;
}
Im very impressed by your work - need more testing to see if it really is as good as advertised in Tailwind projects.
If it is, im creating this issue, maybe even for myself if it wont be done when i finish testing, to create postcss plugin to make it easier to plug into existing asset pipelines.
.. Or maybe you will just merge your performance fixes to purgecss as you did with some of them, and then there will be no need for it :)
Any class name with specific tags in their names will not be dropped even if the html does not contain them.
I explained this second issue poorly in #57
.anyClassNameWithhtml{
font-weight:400;
}
.anyClassNameWithbody{
font-weight:400;
}
.anyClassNameWithhead{
font-weight:400;
}
<html>
<div class="nothing"></div>
</html>
Thanks for everything!
Our html includes <link-formatter>....</link-formatter>
DropCSS incorrrectly matches it with NASTIES ( this part: <link[^>]*>
), and drops the opening tag. The resulting HTML becomes incorrect (unbalanced closing tag).
This probably also applies to the <meta
part of the regex
More a question: using this on a single html page is ok, but in reality most sites comprise many pages.
Is there a way to apply this to a range of pages, or an entire site?
This is the holy grail IMO.
Hey there!
I'd like to report a security issue but cannot find contact instructions on your repository.
If not a hassle, might you kindly add a SECURITY.md
file with an email, or another contact method? GitHub recommends this best practice to ensure security issues are responsibly disclosed, and it would serve as a simple instruction for security researchers in the future.
Thank you for your consideration, and I look forward to hearing from you!
(cc @huntr-helper)
When looking at the repo and the examples I see `didRetain´ being used and available
https://github.com/leeoniya/dropcss/blob/master/dist/dropcss.cjs.js#L972
However, the version on npm does not have it 😅
https://unpkg.com/[email protected]/dist/dropcss.cjs.js
Is it possible to publish the latest version to npm as well?
I want to optimize css file across multiple html files... those css files could be different so it would be nice if you could drop or retain individual declarations. Potentially via didRetainDeclaration
and dropDeclaration
.
it would allow doing something like this...
if you would be interested in supporting this I could prepare a PR for it 🤗
let me know what you think 🤗
Some more details
I currently have it working in a fork and it usage looks like this
dropcss({
html: page.html,
css: page.css,
didRetain: selector => {
let sharedCount = sharedUsage.has(selector) ? sharedUsage.get(selector) : 0;
sharedUsage.set(selector, sharedCount + 1);
},
didRetainDeclaration: ({ selector, property, value }) => {
const key = `${selector} { ${property}: ${value} }`;
let sharedCount = sharedDeclarations.has(key) ? sharedDeclarations.get(key) : 0;
sharedDeclarations.set(key, sharedCount + 1);
},
});
the code change itself is actually rather small and mostly in css.js
function generate(tokens, didRetain, didRetainDeclaration, shouldDropDeclaration) {
let out = '',
lastSelsLen = 0;
for (let i = 0; i < tokens.length; i++) {
let tok = tokens[i];
switch (tok) {
case SELECTORS:
let sels = tokens[++i];
lastSelsLen = sels.length;
if (lastSelsLen > 0) {
sels.forEach(didRetain);
sels.forEach(selector => {
const declarations = tokens[i + 2]
.split(';')
.map(line => line.trim())
.filter(_ => !!_);
let declarationIndex = 0;
for (const declaration of declarations) {
const [property, value] = declaration.split(':').map(value => value.trim());
if (shouldDropDeclaration({ selector, declaration, property, value })) {
declarations[declarationIndex] = null;
} else {
didRetainDeclaration({ selector, declaration, property, value });
}
tokens[i + 2] = declarations.filter(_ => !!_).join(';');
if (tokens[i + 2] !== '') {
tokens[i + 2] += ';';
}
declarationIndex += 1;
}
});
out += sels.join();
}
break;
Right now, rules are removed if the document lacks html
, head
, or body
tags, even though these are implicitly added in HTML 5. The parser should also assume the presence of these tags so that these rules aren't removed.
Example input HTML/CSS:
<!DOCTYPE html>
<p>Hello, world!
html { background: black; color: white }
p { margin: 1em }
Currently outputs CSS:
p { margin: 1em }
Even though it should not remove the html
selector.
Note that this also applies to head
and body
, e.g. head title
will always be equivalent to title
with extra specificity even if no <head>
tag is present and body p
will always be equivalent to p
with extra specificity even if no <body>
tag is present.
would be good for uniformity. right now they're removed in a post-pass without any feedback or notification to the user.
Relatively trimmed example HTML: <!DOCTYPE html><meta charset=UTF-8><div class=dark><section><p>Paragraph</section></div>
Not 100% sure what the cause is. Commented on the line in the commit that fails.
see title!
By the way amazing library and thanks for sharing
Is there a plan for webpack loader?
Several errors occur while parsing pseudo-elements (::first-letter).
This happens when "shouldDrop" returns "false".
There are 2 demo:
https://codepen.io/babakhin/pen/xorYMP
https://codepen.io/babakhin/pen/orwqNM
Error text:
TypeError: Cannot read property '2' of null at next (D:\web projects\test11\node_modules\dropcss\dist\dropcss.cjs.js:523:10) at parse$1 (D:\web projects\test11\node_modules\dropcss\dist\dropcss.cjs.js:566:5) at _export_some_ (D:\web projects\test11\node_modules\dropcss\dist\dropcss.cjs.js:795:48) at D:\web projects\test11\node_modules\dropcss\dist\dropcss.cjs.js:1056:32
How could I integrate dropcss in the nextjs build flow/process?
Was just testing this with the CSS on apple.com for fun. The library will freeze and crash if given this css:
.anything-dash-body{
font-weight:400;
}
.anything-dash-body:lang(ar){
line-height: 1;
}
The library mistakenly thinks .anything-dash-body is a body tag and does not remove it which is "ok", but I don't think it should freeze and crash when failing. All other selectors, etc seem to be ok.
https://www.youtube.com/watch?v=mk7VWcuVOf0
It should be possible to attain at least PurgeCSS speeds, and perhaps better.
Some ideas to give the old college try:
nodes
, parents
and children
into flat Map()
s, this would make it possible to very quickly query/filter leaf nodes (as would be necessary when matching selectors right to left). the current node-html-parser
dependency outputs a tree and does not internally use flat structures, so is likely not terribly efficient to query after parsingcss-select
in favor of letting CSSTree
parse all selectors in its single pass or use https://github.com/fb55/css-what directly, but modify it to return bytecode sequences that do right-to-left matching against the Map objects instead of an intermediate CSS AST.Hi why does this tool doesn't merge these two rules?
.x-btn-group-tl,.x-btn-group-tr{background-image: url(../ext/default/button/group-cs.gif);}
.x-btn-group-bl,.x-btn-group-br{background-image: url(../ext/default/button/group-cs.gif);}
const dropcss = require('dropcss');
var html = "";
var css = ".x-btn-group-tl,.x-btn-group-tr{background-image: url(../ext/default/button/group-cs.gif);}.x-btn-group-bl,.x-btn-group-br{background-image: url(../ext/default/button/group-cs.gif);}"
let cleaned = dropcss({
html,
css,
shouldDrop: (sel) => {
return false;
},
});console.log(cleaned.css);
:has
:matches
because CSS optimization is a slippery slope, currently DropCSS draws a line in the sand for how it can mutate the provided CSS. right now it will only remove CSS selectors, and if all selectors are removed for a specific block, then it will also remove that block. that is to say, it does not touch the contents of the blocks.
dropping unused css variables should be a simple affair with just 2 regex passes, but would deviate from the current block modification abstinence. on the other hand, it still serves the core function of "removing unused css", so maybe i'm making a mountain out of a molehill.
Instead of spamming other libs' issues inviting people to try DropCSS, I'll just cc them here.
An error occurs when dropcss tries to handle and drop lots of font-faces (2 and more).
Error text:
(node:11700) UnhandledPromiseRejectionWarning: TypeError: Cannot read property '1' of null
at dropFontFaces (D:\web projects\test4\node_modules\dropcss\dist\dropcss.cjs.js:896:33)
at postProc$1 (D:\web projects\test4\node_modules\dropcss\dist\dropcss.cjs.js:935:8)
at dropcss (D:\web projects\test4\node_modules\dropcss\dist\dropcss.cjs.js:1062:8)
...
Demo:
Codepen
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.