All Projects → leeoniya → Dropcss

leeoniya / Dropcss

Licence: mit
An exceptionally fast, thorough and tiny unused-CSS cleaner

Programming Languages

HTML
75241 projects
CSS
56736 projects
javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Dropcss

js-cleanup
Smart comment and whitespace cleaner for JavaScript-like files
Stars: ✭ 22 (-98.95%)
Mutual labels:  clean, remove
remove-files-webpack-plugin
A plugin for webpack that removes files and folders before and after compilation.
Stars: ✭ 48 (-97.72%)
Mutual labels:  clean, remove
strip-markdown
plugin remove Markdown formatting
Stars: ✭ 84 (-96%)
Mutual labels:  clean, remove
lint-deps
Lint for unused or missing dependencies in your node.js projects. Customize with plugins or configuration.
Stars: ✭ 48 (-97.72%)
Mutual labels:  remove, unused
unimport
A linter, formatter for finding and removing unused import statements.
Stars: ✭ 119 (-94.34%)
Mutual labels:  remove, unused
V8 Natives
Access v8 Engine Natives easily in Chrome & Node
Stars: ✭ 161 (-92.34%)
Mutual labels:  optimization
Fe Performance Journey
🚵 a Journey of Performance Optimizing in Frontend 🚀
Stars: ✭ 169 (-91.96%)
Mutual labels:  optimization
Cpmoptimize
🚀 🐍 Optimizes Python bytecode calculating linear recurrences
Stars: ✭ 159 (-92.44%)
Mutual labels:  optimization
Cleanlinks
Converts obfuscated/nested links to genuine clean links.
Stars: ✭ 157 (-92.53%)
Mutual labels:  clean
Openwrt Sfe Flowoffload Ath79
Openwrt firmware with SFE and FlowOffload
Stars: ✭ 178 (-91.53%)
Mutual labels:  optimization
Mlrmbo
Toolbox for Bayesian Optimization and Model-Based Optimization in R
Stars: ✭ 173 (-91.77%)
Mutual labels:  optimization
Sporco
Sparse Optimisation Research Code
Stars: ✭ 164 (-92.2%)
Mutual labels:  optimization
One
On-device Neural Engine
Stars: ✭ 162 (-92.29%)
Mutual labels:  optimization
Mathtoolbox
Mathematical tools (interpolation, dimensionality reduction, optimization, etc.) written in C++11 with Eigen
Stars: ✭ 172 (-91.82%)
Mutual labels:  optimization
Far Ho
Gradient based hyperparameter optimization & meta-learning package for TensorFlow
Stars: ✭ 161 (-92.34%)
Mutual labels:  optimization
Scikit Opt
Genetic Algorithm, Particle Swarm Optimization, Simulated Annealing, Ant Colony Optimization Algorithm,Immune Algorithm, Artificial Fish Swarm Algorithm, Differential Evolution and TSP(Traveling salesman)
Stars: ✭ 2,791 (+32.78%)
Mutual labels:  optimization
Router
⚡️ A lightning fast HTTP router
Stars: ✭ 158 (-92.48%)
Mutual labels:  optimization
Clean Code Zh
《代码整洁之道》中文翻译
Stars: ✭ 164 (-92.2%)
Mutual labels:  clean
Iminuit
Jupyter-friendly Python interface for C++ MINUIT2
Stars: ✭ 172 (-91.82%)
Mutual labels:  optimization
Rl Baselines3 Zoo
A collection of pre-trained RL agents using Stable Baselines3, training and hyperparameter optimization included.
Stars: ✭ 161 (-92.34%)
Mutual labels:  optimization

🗑 DropCSS

An exceptionally fast, thorough and tiny (~10 KB min) unused-CSS cleaner (MIT Licensed)


Introduction

DropCSS takes your HTML and CSS as input and returns only the used CSS as output. Its custom HTML and CSS parsers are highly optimized for the 99% use case and thus avoid the overhead of handling malformed markup or stylesheets, so well-formed input is required. There is minimal handling for complex escaping rules, so there will always exist cases of valid input that cannot be processed by DropCSS; for these infrequent cases, please start a discussion. While the HTML spec allows html, head, body and tbody to be implied/omitted, DropCSS makes no such assumptions; selectors will only be retained for tags that can be parsed from provided markup.

It's also a good idea to run your CSS through a structural optimizer like clean-css, csso, cssnano or crass to re-group selectors, merge redundant rules, etc. It probably makes sense to do this after DropCSS, which can leave redundant blocks, e.g. .foo, .bar { color: red; } .bar { width: 50%; } -> .bar { color: red; } .bar { width: 50%; } if .foo is absent from your markup.

More on this project's backstory & discussions: v0.1.0 alpha: /r/javascript, Hacker News and v1.0.0 release: /r/javascript.


Live Demo: https://codepen.io/leeoniya/pen/LvbRyq


Installation

npm install -D dropcss

Usage & API

const dropcss = require('dropcss');

let html = `
    <html>
        <head></head>
        <body>
            <p>Hello World!</p>
        </body>
    </html>
`;

let css = `
    .card {
      padding: 8px;
    }

    p:hover a:first-child {
      color: red;
    }
`;

const whitelist = /#foo|\.bar/;

let dropped = new Set();

let cleaned = dropcss({
    html,
    css,
    shouldDrop: (sel) => {
        if (whitelist.test(sel))
            return false;
        else {
            dropped.add(sel);
            return true;
        }
    },
});

console.log(cleaned.css);

console.log(dropped);

The shouldDrop hook is called for every CSS selector that could not be matched in the html. Return false to retain the selector or true to drop it.


Features

  • Supported selectors

    Common Attribute Positional Positional (of-type) Other
    * - universal
    <tag> - tag
    # - id
    . - class
      - descendant
    > - child
    + - adjacent sibling
    ~ - general sibling
    [attr]
    [attr=val]
    [attr*=val]
    [attr^=val]
    [attr$=val]
    [attr~=val]
    :first-child
    :last-child
    :only-child
    :nth-child()
    :nth-last-child()
    :first-of-type
    :last-of-type
    :only-of-type
    :nth-of-type()
    :nth-last-of-type()
    :not()
  • Retention of all transient pseudo-class and pseudo-element selectors which cannot be deterministically checked from the parsed HTML.

  • Removal of unused @font-face and @keyframes blocks.

  • Removal of unused CSS variables.

  • Deep resolution of composite CSS variables, e.g:

    :root {
      --font-style: italic;
      --font-weight: bold;
      --line-height: var(--height)em;
      --font-family: 'Open Sans';
      --font: var(--font-style) var(--font-weight) 1em/var(--line-height) var(--font-family);
      --height: 1.6;
    }
    
    @font-face {
      font-family: var(--font-family);
      src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
           url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
    }
    
    body {
      font: var(--font);
    }

Performance

Input

test.html

  • 18.8 KB minified
  • 502 dom nodes via document.querySelectorAll("*").length

styles.min.css

  • 27.67 KB combined, optimized and minified via clean-css
  • contents: Bootstrap's reboot.css, an in-house flexbox grid, global layout, navbars, colors & page-specific styles. (the grid accounts for ~85% of this starting weight, lots of media queries & repetition)

Output

lib size w/deps output size reduction time elapsed unused bytes (test.html coverage)
DropCSS 58.4 KB
6 Files, 2 Folders
6.58 KB 76.15% 21 ms 575 / 8.5%
UnCSS 13.5 MB
2,829 Files, 301 Folders
6.72 KB 75.71% 385 ms 638 / 9.3%
Purgecss 2.69 MB
560 Files, 119 Folders
8.01 KB 71.05% 88 ms 1,806 / 22.0%
PurifyCSS 3.46 MB
792 Files, 207 Folders
15.46 KB 44.34% 173 ms 9,440 / 59.6%

Notes

  • About 400 "unused bytes" are due to an explicit/shared whitelist, not an inability of the tools to detect/remove that CSS.
  • About 175 "unused bytes" are due to vendor-prefixed (-moz, -ms) properties & selectors that are inactive in Chrome, which is used for testing coverage.
  • Purgecss does not support attribute or complex selectors: Issue #110.

A full Stress Test is also available.


JavaScript Execution

DropCSS does not load external resources or execute <script> tags, so your HTML must be fully formed (or SSR'd). Alternatively, you can use Puppeteer and a local http server to get full <script> execution.

Here's a 35 line script which does exactly that:

const httpServer = require('http-server');
const puppeteer = require('puppeteer');
const fetch = require('node-fetch');
const dropcss = require('dropcss');

const server = httpServer.createServer({root: './www'});
server.listen(8080);

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('http://127.0.0.1:8080/index.html');
    const html = await page.content();
    const styleHrefs = await page.$$eval('link[rel=stylesheet]', els => Array.from(els).map(s => s.href));
    await browser.close();

    await Promise.all(styleHrefs.map(href =>
        fetch(href).then(r => r.text()).then(css => {
            let start = +new Date();

            let clean = dropcss({
                css,
                html,
            });

            console.log({
                stylesheet: href,
                cleanCss: clean.css,
                elapsed: +new Date() - start,
            });
        })
    ));

    server.close();
})();

Accumulating a Whitelist

Perhaps you want to take one giant CSS file and purge it against multiple HTML sources, thus retaining any selectors that appear in any HTML source. This also applies when using Puppeteer to invoke different application states to ensure that DropCSS takes every state into account before cleaning the CSS. The idea is rather simple:

  1. Run DropCSS against each HTML source.
  2. Accumulate a whitelist from each result.
  3. Run DropCSS against an empty HTML string, relying only on the accumulated whitelist.

See /demos/accumulate.js:

const dropcss = require('dropcss');

// super mega-huge combined stylesheet
let css = `
    em {
        color: red;
    }

    p {
        font-weight: bold;
    }

    .foo {
        font-size: 10pt;
    }
`;

// html of page (or state) A
let htmlA = `
    <html>
        <head></head>
        <body>
            <em>Hello World!</em>
        </body>
    </html>
`;

// html of page (or state) B
let htmlB = `
    <html>
        <head></head>
        <body>
            <p>Soft Kitties!</p>
        </body>
    </html>
`;

// whitelist
let whitelist = new Set();

function didRetain(sel) {
    whitelist.add(sel);
}

let resA = dropcss({
    css,
    html: htmlA,
    didRetain,
});

let resB = dropcss({
    css,
    html: htmlB,
    didRetain,
});

// final purge relying only on accumulated whitelist
let cleaned = dropcss({
    html: '',
    css,
    shouldDrop: sel => !whitelist.has(sel),
});

console.log(cleaned.css);

Special / Escaped Sequences

DropCSS is stupid and will choke on unusual selectors, like the ones used by the popular Tailwind CSS framework:

class attributes can look like this:

<div class="px-6 pt-6 overflow-y-auto text-base lg:text-sm lg:py-12 lg:pl-6 lg:pr-8 sticky?lg:h-(screen-16)"></div>
<div class="px-2 -mx-2 py-1 transition-fast relative block hover:translate-r-2px hover:text-gray-900 text-gray-600 font-medium"></div>

...and the CSS looks like this:

.sticky\?lg\:h-\(screen-16\){...}
.lg\:text-sm{...}
.lg\:focus\:text-green-700:focus{...}

Ouch.

The solution is to temporarily replace the escaped characters in the HTML and CSS with some unique strings which match /[\w-]/. This allows DropCSS's tokenizer to consider the classname as one contiguous thing. After processing, we simply reverse the operation.

// 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')
);

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

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

This performant work-around allows DropCSS to process Tailwind without issues \o/ and is easily adaptable to support other "interesting" cases. One thing to keep in mind is that shouldDrop() will be called with selectors containing the temp replacements rather than original selectors, so make sure to account for this if shouldDrop() is used to test against some whitelist.


Caveats

  • Not tested against or designd to handle malformed HTML or CSS
  • Excessive escaping or reserved characters in your HTML or CSS can break DropCSS's parsers

Acknowledgements

  • Felix Böhm's nth-check - it's not much code, but getting An+B expression testing exactly right is frustrating. I got part-way there before discovering this tiny solution.
  • Vadim Kiryukhin's vkbeautify - the benchmark and test code uses this tiny formatter to make it easier to spot differences in output diffs.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].