{"id":13394064,"url":"https://github.com/leeoniya/dropcss","last_synced_at":"2025-05-14T19:01:54.587Z","repository":{"id":34306309,"uuid":"176791030","full_name":"leeoniya/dropcss","owner":"leeoniya","description":"An exceptionally fast, thorough and tiny unused-CSS cleaner","archived":false,"fork":false,"pushed_at":"2023-08-24T19:27:19.000Z","size":545,"stargazers_count":2136,"open_issues_count":15,"forks_count":68,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-13T13:57:22.494Z","etag":null,"topics":["clean","css","optimization","purge","remove","styles","unused"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leeoniya.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-03-20T18:08:24.000Z","updated_at":"2025-04-09T09:39:10.000Z","dependencies_parsed_at":"2024-01-18T15:57:06.187Z","dependency_job_id":"571ae020-bb9b-4b7f-8b2d-4556ce8c8312","html_url":"https://github.com/leeoniya/dropcss","commit_stats":{"total_commits":148,"total_committers":1,"mean_commits":148.0,"dds":0.0,"last_synced_commit":"cf9d1264fa048b908baedec7be607538c87a3fea"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Fdropcss","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Fdropcss/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Fdropcss/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Fdropcss/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leeoniya","download_url":"https://codeload.github.com/leeoniya/dropcss/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254029175,"owners_count":22002312,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clean","css","optimization","purge","remove","styles","unused"],"created_at":"2024-07-30T17:01:07.758Z","updated_at":"2025-05-14T19:01:54.025Z","avatar_url":"https://github.com/leeoniya.png","language":"HTML","readme":"## 🗑 DropCSS\n\nAn [exceptionally fast](#performance), thorough and tiny ([~10 KB min](https://github.com/leeoniya/dropcss/tree/master/dist/dropcss.iife.min.js)) unused-CSS cleaner _(MIT Licensed)_\n\n---\n### Introduction\n\nDropCSS 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](https://github.com/leeoniya/dropcss/issues). 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.\n\nIt's also a good idea to run your CSS through a structural optimizer like [clean-css](https://github.com/jakubpawlowicz/clean-css), [csso](https://github.com/css/csso), [cssnano](https://github.com/cssnano/cssnano) or [crass](https://github.com/mattbasta/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%; }` -\u003e `.bar { color: red; } .bar { width: 50%; }` if `.foo` is absent from your markup.\n\nMore on this project's backstory \u0026 discussions: v0.1.0 alpha: [/r/javascript](https://old.reddit.com/r/javascript/comments/b3mcu8/dropcss_010_a_minimal_and_thorough_unused_css/), [Hacker News](https://news.ycombinator.com/item?id=19469080) and v1.0.0 release: [/r/javascript](https://old.reddit.com/r/javascript/comments/bb7im2/dropcss_v100_an_exceptionally_fast_thorough_and/).\n\n---\n\u003ch3 align=\"center\"\u003eLive Demo: \u003ca href=\"https://codepen.io/leeoniya/pen/LvbRyq\"\u003ehttps://codepen.io/leeoniya/pen/LvbRyq\u003c/a\u003e\u003c/h3\u003e\n\n---\n### Installation\n\n```\nnpm install -D dropcss\n```\n\n---\n### Usage \u0026 API\n\n```js\nconst dropcss = require('dropcss');\n\nlet html = `\n    \u003chtml\u003e\n        \u003chead\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cp\u003eHello World!\u003c/p\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\n`;\n\nlet css = `\n    .card {\n      padding: 8px;\n    }\n\n    p:hover a:first-child {\n      color: red;\n    }\n`;\n\nconst whitelist = /#foo|\\.bar/;\n\nlet dropped = new Set();\n\nlet cleaned = dropcss({\n    html,\n    css,\n    shouldDrop: (sel) =\u003e {\n        if (whitelist.test(sel))\n            return false;\n        else {\n            dropped.add(sel);\n            return true;\n        }\n    },\n});\n\nconsole.log(cleaned.css);\n\nconsole.log(dropped);\n```\n\nThe `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.\n\n---\n### Features\n\n- Supported selectors\n\n  | Common                                                                                                                                            | Attribute                                                                                    | Positional                                                                                | Positional (of-type)                                                                                | Other    |\n  |---------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|----------|\n  | `*` - universal\u003cbr\u003e`\u003ctag\u003e` - tag\u003cbr\u003e`#` - id\u003cbr\u003e`.` - class\u003cbr\u003e\u003ccode\u003e\u0026nbsp;\u003c/code\u003e - descendant\u003cbr\u003e`\u003e` - child\u003cbr\u003e`+` - adjacent sibling\u003cbr\u003e`~` - general sibling | `[attr]`\u003cbr\u003e`[attr=val]`\u003cbr\u003e`[attr*=val]`\u003cbr\u003e`[attr^=val]`\u003cbr\u003e`[attr$=val]`\u003cbr\u003e`[attr~=val]` | `:first-child`\u003cbr\u003e`:last-child`\u003cbr\u003e`:only-child`\u003cbr\u003e`:nth-child()`\u003cbr\u003e`:nth-last-child()` | `:first-of-type`\u003cbr\u003e`:last-of-type`\u003cbr\u003e`:only-of-type`\u003cbr\u003e`:nth-of-type()`\u003cbr\u003e`:nth-last-of-type()` | `:not()` |\n\n- Retention of all transient pseudo-class and pseudo-element selectors which cannot be deterministically checked from the parsed HTML.\n- Removal of unused `@font-face` and `@keyframes` blocks.\n- Removal of unused [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/--*).\n- Deep resolution of composite CSS variables, e.g:\n\n  ```css\n  :root {\n    --font-style: italic;\n    --font-weight: bold;\n    --line-height: var(--height)em;\n    --font-family: 'Open Sans';\n    --font: var(--font-style) var(--font-weight) 1em/var(--line-height) var(--font-family);\n    --height: 1.6;\n  }\n\n  @font-face {\n    font-family: var(--font-family);\n    src: url(\"/fonts/OpenSans-Regular-webfont.woff2\") format(\"woff2\"),\n         url(\"/fonts/OpenSans-Regular-webfont.woff\") format(\"woff\");\n  }\n\n  body {\n    font: var(--font);\n  }\n  ```\n\n---\n### Performance\n\n#### Input\n\n**test.html**\n\n- 18.8 KB minified\n- 502 dom nodes via `document.querySelectorAll(\"*\").length`\n\n**styles.min.css**\n\n- 27.67 KB combined, optimized and minified via [clean-css](https://github.com/jakubpawlowicz/clean-css)\n- contents: Bootstrap's [reboot.css](https://github.com/twbs/bootstrap/blob/master/dist/css/bootstrap-reboot.css), an in-house flexbox grid, global layout, navbars, colors \u0026 page-specific styles. (the grid accounts for ~85% of this starting weight, lots of media queries \u0026 repetition)\n\n#### Output\n\n\u003ctable\u003e\n    \u003cthead\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003c/th\u003e\n            \u003cth\u003elib size w/deps\u003c/th\u003e\n            \u003cth\u003eoutput size\u003c/th\u003e\n            \u003cth\u003ereduction\u003c/th\u003e\n            \u003cth\u003etime elapsed\u003c/th\u003e\n            \u003cth\u003eunused bytes (test.html coverage)\u003c/th\u003e\n        \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003cstrong\u003eDropCSS\u003c/strong\u003e\u003c/th\u003e\n            \u003ctd\u003e\n                58.4 KB\u003cbr\u003e\n                6 Files, 2 Folders\n            \u003c/td\u003e\n            \u003ctd\u003e6.58 KB\u003c/td\u003e\n            \u003ctd\u003e76.15%\u003c/td\u003e\n            \u003ctd\u003e21 ms\u003c/td\u003e\n            \u003ctd\u003e575 / 8.5%\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003ca href=\"https://github.com/uncss/uncss\"\u003eUnCSS\u003c/a\u003e\u003c/th\u003e\n            \u003ctd\u003e\n                13.5 MB\u003cbr\u003e\n                2,829 Files, 301 Folders\n            \u003c/td\u003e\n            \u003ctd\u003e6.72 KB\u003c/td\u003e\n            \u003ctd\u003e75.71%\u003c/td\u003e\n            \u003ctd\u003e385 ms\u003c/td\u003e\n            \u003ctd\u003e638 / 9.3%\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003ca href=\"https://github.com/FullHuman/purgecss\"\u003ePurgecss\u003c/a\u003e\u003c/th\u003e\n            \u003ctd\u003e\n                2.69 MB\u003cbr\u003e\n                560 Files, 119 Folders\n            \u003c/td\u003e\n            \u003ctd\u003e8.01 KB\u003c/td\u003e\n            \u003ctd\u003e71.05%\u003c/td\u003e\n            \u003ctd\u003e88 ms\u003c/td\u003e\n            \u003ctd\u003e1,806 / 22.0%\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003ca href=\"https://github.com/purifycss/purifycss\"\u003ePurifyCSS\u003c/a\u003e\u003c/th\u003e\n            \u003ctd\u003e\n                3.46 MB\u003cbr\u003e\n                792 Files, 207 Folders\n            \u003c/td\u003e\n            \u003ctd\u003e15.46 KB\u003c/td\u003e\n            \u003ctd\u003e44.34%\u003c/td\u003e\n            \u003ctd\u003e173 ms\u003c/td\u003e\n            \u003ctd\u003e9,440 / 59.6%\u003c/td\u003e\n        \u003c/tr\u003e\n    \u003c/tbody\u003e\n\u003c/table\u003e\n\n**Notes**\n\n- About 400 \"unused bytes\" are due to an explicit/shared whitelist, not an inability of the tools to detect/remove that CSS.\n- About 175 \"unused bytes\" are due to vendor-prefixed (-moz, -ms) properties \u0026 selectors that are inactive in Chrome, which is used for testing coverage.\n- Purgecss does not support attribute or complex selectors: [Issue #110](https://github.com/FullHuman/purgecss/issues/110).\n\nA full **[Stress Test](https://github.com/leeoniya/dropcss/tree/master/test/bench)** is also available.\n\n---\n### JavaScript Execution\n\nDropCSS does not load external resources or execute `\u003cscript\u003e` tags, so your HTML must be fully formed (or SSR'd). Alternatively, you can use [Puppeteer](https://github.com/GoogleChrome/puppeteer) and a local http server to get full `\u003cscript\u003e` execution.\n\n[Here's a 35 line script](/demos/puppeteer/index.js) which does exactly that:\n\n```js\nconst httpServer = require('http-server');\nconst puppeteer = require('puppeteer');\nconst fetch = require('node-fetch');\nconst dropcss = require('dropcss');\n\nconst server = httpServer.createServer({root: './www'});\nserver.listen(8080);\n\n(async () =\u003e {\n    const browser = await puppeteer.launch();\n    const page = await browser.newPage();\n    await page.goto('http://127.0.0.1:8080/index.html');\n    const html = await page.content();\n    const styleHrefs = await page.$$eval('link[rel=stylesheet]', els =\u003e Array.from(els).map(s =\u003e s.href));\n    await browser.close();\n\n    await Promise.all(styleHrefs.map(href =\u003e\n        fetch(href).then(r =\u003e r.text()).then(css =\u003e {\n            let start = +new Date();\n\n            let clean = dropcss({\n                css,\n                html,\n            });\n\n            console.log({\n                stylesheet: href,\n                cleanCss: clean.css,\n                elapsed: +new Date() - start,\n            });\n        })\n    ));\n\n    server.close();\n})();\n```\n\n---\n### Accumulating a Whitelist\n\nPerhaps 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:\n\n1. Run DropCSS against each HTML source.\n2. Accumulate a whitelist from each result.\n3. Run DropCSS against an empty HTML string, relying only on the accumulated whitelist.\n\nSee [/demos/accumulate.js](/demos/accumulate.js):\n\n```js\nconst dropcss = require('dropcss');\n\n// super mega-huge combined stylesheet\nlet css = `\n    em {\n        color: red;\n    }\n\n    p {\n        font-weight: bold;\n    }\n\n    .foo {\n        font-size: 10pt;\n    }\n`;\n\n// html of page (or state) A\nlet htmlA = `\n    \u003chtml\u003e\n        \u003chead\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cem\u003eHello World!\u003c/em\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\n`;\n\n// html of page (or state) B\nlet htmlB = `\n    \u003chtml\u003e\n        \u003chead\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cp\u003eSoft Kitties!\u003c/p\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\n`;\n\n// whitelist\nlet whitelist = new Set();\n\nfunction didRetain(sel) {\n    whitelist.add(sel);\n}\n\nlet resA = dropcss({\n    css,\n    html: htmlA,\n    didRetain,\n});\n\nlet resB = dropcss({\n    css,\n    html: htmlB,\n    didRetain,\n});\n\n// final purge relying only on accumulated whitelist\nlet cleaned = dropcss({\n    html: '',\n    css,\n    shouldDrop: sel =\u003e !whitelist.has(sel),\n});\n\nconsole.log(cleaned.css);\n```\n\n---\n### Special / Escaped Sequences\n\nDropCSS is stupid and will choke on unusual selectors, like the ones used by the popular [Tailwind CSS](https://github.com/tailwindcss/tailwindcss) framework:\n\n`class` attributes can look like this:\n\n```html\n\u003cdiv 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)\"\u003e\u003c/div\u003e\n\u003cdiv class=\"px-2 -mx-2 py-1 transition-fast relative block hover:translate-r-2px hover:text-gray-900 text-gray-600 font-medium\"\u003e\u003c/div\u003e\n```\n\n...and the CSS looks like this:\n\n```css\n.sticky\\?lg\\:h-\\(screen-16\\){...}\n.lg\\:text-sm{...}\n.lg\\:focus\\:text-green-700:focus{...}\n```\n\nOuch.\n\nThe 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.\n\n```js\n// remap\nlet css2 = css\n    .replace(/\\\\\\:/gm, '__0')\n    .replace(/\\\\\\//gm, '__1')\n    .replace(/\\\\\\?/gm, '__2')\n    .replace(/\\\\\\(/gm, '__3')\n    .replace(/\\\\\\)/gm, '__4');\n\nlet html2 = html.replace(/class=[\"'][^\"']*[\"']/gm, m =\u003e\n    m\n    .replace(/\\:/gm, '__0')\n    .replace(/\\//gm, '__1')\n    .replace(/\\?/gm, '__2')\n    .replace(/\\(/gm, '__3')\n    .replace(/\\)/gm, '__4')\n);\n\nlet res = dropcss({\n    css: css2,\n    html: html2,\n});\n\n// undo\nres.css = res.css\n    .replace(/__0/gm, '\\\\:')\n    .replace(/__1/gm, '\\\\/')\n    .replace(/__2/gm, '\\\\?')\n    .replace(/__3/gm, '\\\\(')\n    .replace(/__4/gm, '\\\\)');\n```\n\nThis 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.\n\n---\n### Caveats\n\n- Not tested against or designd to handle malformed HTML or CSS\n- Excessive escaping or reserved characters in your HTML or CSS can break DropCSS's parsers\n\n---\n### Acknowledgements\n\n- Felix Böhm's [nth-check](https://github.com/fb55/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.\n- Vadim Kiryukhin's [vkbeautify](https://github.com/vkiryukhin/vkBeautify) - the benchmark and test code uses this tiny formatter to make it easier to spot differences in output diffs.","funding_links":[],"categories":["HTML","工具库"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleeoniya%2Fdropcss","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleeoniya%2Fdropcss","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleeoniya%2Fdropcss/lists"}