purify-css 和 uncss 都是用于从项目中移除未使用 CSS 规则的构建时工具,旨在减小最终打包文件的体积并提升加载性能。它们通过分析源代码(HTML、JS、模板文件)中实际使用的类名、ID 和标签,对比输入的 CSS 文件,从而生成仅包含必要样式的精简版本。尽管目标一致,但两者在解析策略、对动态内容的支持以及当前的维护状态上存在显著差异,直接影响其在现代前端架构中的适用性。
在前端性能优化中,移除未使用的 CSS 是减小包体积的关键步骤。purify-css 和 uncss 曾是该领域的两个主流选择,但它们的工作原理截然不同,导致了不同的适用场景和维护命运。本文将深入剖析两者的技术实现、代码示例及架构决策要点。
purify-css 采用静态文件扫描策略。它读取你指定的源代码文件(HTML, JS, Twig, Handlebars 等),通过正则表达式和简单的 AST 分析提取类名和 ID。它不运行代码,因此速度极快,但无法感知运行时动态添加的类。
// purify-css: 显式指定源文件路径进行静态扫描
const Purify = require('purify-css');
Purify.purify(
['src/**/*.html', 'src/**/*.js'], // 扫描这些文件中的类名
['src/css/main.css'], // 待清理的 CSS 文件
{
whitelist: ['always-keep-this-class'], // 手动保护动态类
rejected: true // 输出被删除的选择器列表
},
function(result) {
console.log(result); // 输出精简后的 CSS
}
);
uncss 则采用动态渲染策略。它在内部启动一个无头浏览器(历史上是 PhantomJS),加载你的 HTML 页面,执行 JavaScript,然后检查渲染后的 DOM 中实际存在哪些元素,最后保留这些元素对应的样式。这种方法能捕捉到 JS 生成的类,但速度慢且依赖沉重的运行时环境。
// uncss: 指定 URL 或本地文件,通过渲染提取样式
const uncss = require('uncss');
const options = {
html: ['dist/index.html', 'dist/about.html'], // 加载这些页面进行渲染
ignore: ['.js-generated-class'], // 忽略某些选择器
timeout: 500 // 等待 JS 执行的时间
};
uncss(options, (error, output) => {
if (error) throw error;
console.log(output); // 输出基于渲染结果的 CSS
});
这是架构选型中最关键的一点。uncss 已处于事实上的废弃状态。其核心依赖 phantomjs 早已停止维护,存在已知安全漏洞且无法在现代 Node.js 版本中稳定运行。GitHub 仓库长期无实质性更新,社区已普遍转向其他方案。
purify-css 虽然比 uncss 存活时间稍长,但也面临维护频率降低的问题。它的算法对于极其复杂的现代框架(如 React/Vue 的动态组件)可能仍显不足,需要大量手动配置 whitelist。
⚠️ 架构师建议:在生产环境中,不建议在新项目中直接使用
uncss。对于purify-css,需谨慎评估其是否足以覆盖你的动态类名场景。目前业界更推荐使用 PurgeCSS 作为两者的现代替代品,它结合了静态分析的速度和对现代框架(PostCSS, Webpack, Gulp 插件)的良好支持,且维护活跃。
处理 JavaScript 动态添加的类名是此类工具的痛点。
purify-css 无法自动检测运行时类名。如果类名是通过变量拼接或条件渲染生成的,你必须手动将其加入白名单,否则会被误删。
// 场景:JS 中动态添加类名 'dynamic-item'
// document.getElementById('box').classList.add('dynamic-item');
// purify-css 配置:必须手动告知工具保留该类
Purify.purify(
['src/**/*.js'],
['src/css/style.css'],
{
whitelist: ['dynamic-item', /^btn-/] // 手动白名单,支持正则
},
callback
);
uncss 理论上能自动捕获动态类名,前提是它们在页面加载后的指定 timeout 时间内被渲染出来。但这引入了不确定性:如果动画延迟过长或交互触发类名,uncss 可能会漏掉。
// 场景:JS 动态添加类名
// uncss 尝试通过渲染捕获
const options = {
html: ['src/index.html'],
timeout: 2000, // 必须设置足够长的时间等待 JS 执行
js: ['src/app.js'] // 尝试加载 JS 文件(支持有限)
};
// 风险:如果类名由用户点击触发,uncss 依然无法捕获
uncss(options, callback);
purify-css 更像是一个独立的库,虽然也有 Gulp/Grunt 插件,但它倾向于让你控制文件流。它不关心你的 CSS 预处理器,只关心最终生成的 CSS 字符串。
// 典型工作流:先编译 Sass/Less,再传入 purify-css
const fs = require('fs');
const Purify = require('purify-css');
const css = fs.readFileSync('dist/bundle.css', 'utf8');
// 直接处理 CSS 字符串
Purify.purify(['dist/**/*.html'], css, options, (result) => {
fs.writeFileSync('dist/final.css', result);
});
uncss 通常作为构建流程的最后一步,直接操作 HTML 文件路径。它对预处理器的支持是通过先编译好 CSS 再传入实现的,但其配置强依赖于文件路径和网络 URL。
// 典型工作流:指向编译后的 HTML 文件
const options = {
html: ['http://localhost:8080/index.html'], // 甚至支持远程 URL
css: ['dist/bundle.css']
};
uncss(options, (err, output) => {
// 直接输出结果
});
| 特性 | purify-css | uncss |
|---|---|---|
| 分析方式 | 静态文本扫描 (正则/AST) | 动态浏览器渲染 (PhantomJS) |
| 执行速度 | ⚡ 快 (毫秒级) | 🐢 慢 (秒级,需启动浏览器) |
| 动态类名支持 | ❌ 需手动白名单 | ✅ 自动捕获 (限加载时渲染) |
| 依赖环境 | 轻量 (Node.js 原生) | 沉重 (需无头浏览器环境) |
| 维护状态 | ⚠️ 低频维护 | 🛑 已废弃/不可用 |
| 适用场景 | 传统多页应用,简单 SPA | 遗留项目 (不推荐新项目) |
在现代前端架构中,选择 CSS 清理工具不应仅在 purify-css 和 uncss 之间纠结,因为它们都已显露疲态。
purify-css 类似的静态分析能力,但拥有更活跃的社区、更好的 Tree-shaking 集成(支持 Webpack, Rollup, Vite 插件),以及对 Safelist(白名单)更友好的配置语法。uncss 且构建经常失败(通常因 PhantomJS 兼容性问题),应尽快迁移到 PurgeCSS。purify-css 且能满足需求,可暂时保留,但务必建立完善的视觉回归测试,防止误删样式,并规划未来的迁移路径。核心原则:CSS 清理工具应作为构建流程中“可预测”的一环。依赖浏览器渲染(如 uncss)引入了过多的不确定性和环境依赖,这在 CI/CD 流水线中是巨大的隐患。静态分析配合精确的白名单机制(如 purify-css 或 PurgeCSS 的做法)才是可维护的长久之计。
仅当你的项目是传统的多页应用(MPA),且所有 HTML 内容均在构建时完全静态生成时,才可考虑 uncss。它依赖于 PhantomJS 渲染页面来提取样式,这对静态内容非常精准且配置简单。但鉴于其核心依赖已严重过时且项目长期未维护,在新项目中强烈不建议使用;若必须使用,需做好自行修复底层依赖或迁移到其他活跃工具(如 PurgeCSS)的准备。
如果你的项目包含大量动态生成的类名(如 JavaScript 操作的类、模板字符串或现代框架组件),应选择 purify-css。它通过显式配置白名单和更灵活的源文件扫描机制,能够更安全地处理运行时逻辑,避免误删关键样式。虽然其社区活跃度也有所下降,但其算法对非静态内容的容错率更高,适合复杂的单页应用(SPA)。
UnCSS is a tool that removes unused CSS from your stylesheets. It works across multiple files and supports Javascript-injected CSS.
The process by which UnCSS removes the unused rules is as follows:
document.querySelector filters out selectors that are not found in the HTML files.Please note:
ignore option to preserve classes that are added by Javascript on user interaction.npm install -g uncss
var uncss = require('uncss');
var files = ['my', 'array', 'of', 'HTML', 'files', 'or', 'http://urls.com'],
options = {
banner : false,
csspath : '../public/css/',
htmlroot : 'public',
ignore : ['#added_at_runtime', /test\-[0-9]+/],
ignoreSheets : [/fonts.googleapis/],
inject : function(window) { window.document.querySelector('html').classList.add('no-csscalc', 'csscalc'); },
jsdom : {
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X)',
},
media : ['(min-width: 700px) handheld and (orientation: landscape)'],
raw : 'h1 { color: green }',
report : false,
strictSSL : true,
stylesheets : ['lib/bootstrap/dist/css/bootstrap.css', 'src/public/css/main.css'],
timeout : 1000,
uncssrc : '.uncssrc',
userAgent : 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X)',
};
uncss(files, options, function (error, output) {
console.log(output);
});
/* Look Ma, no options! */
uncss(files, function (error, output) {
console.log(output);
});
/* Specifying raw HTML */
var rawHtml = '...';
uncss(rawHtml, options, function (error, output) {
console.log(output);
});
UnCSS can also be used in conjunction with other JavaScript build systems, such as Grunt, Broccoli or Gulp!
Usage: uncss [options] <file or URL, ...>
e.g. uncss https://getbootstrap.com/docs/3.3/examples/jumbotron/ > stylesheet.css
Options:
-h, --help output usage information
-V, --version output the version number
-i, --ignore <selector, ...> Do not remove given selectors
-m, --media <media_query, ...> Process additional media queries
-C, --csspath <path> Relative path where the CSS files are located
-s, --stylesheets <file, ...> Specify additional stylesheets to process
-S, --ignoreSheets <selector, ...> Do not include specified stylesheets
-r, --raw <string> Pass in a raw string of CSS
-t, --timeout <milliseconds> Wait for JS evaluation
-H, --htmlroot <folder> Absolute paths' root location
-u, --uncssrc <file> Load these options from <file>
-n, --noBanner Disable banner
-a, --userAgent <string> Use a custom user agent string
-I, --inject <file> Path to javascript file to be executed before uncss runs
-o, --output <file> Path to write resulting CSS to
Note that you can pass both local file paths (which are processed by glob) and URLs to the program.
banner (boolean, default: true): Whether a banner should be prepended before each file block in the processed CSS.
csspath (string): Path where the CSS files are related to the HTML files. By default, UnCSS uses the path specified in the <link rel="stylesheet" href="path/to/file.css"/>.
htmlroot (string): Where the project root is. Useful for example if you have HTML that references local files with root-relative URLs, i.e. href="/css/style.css".
ignore (string[]): provide a list of selectors that should not be removed by UnCSS. For example, styles added by user interaction with the page (hover, click), since those are not detectable by UnCSS yet. Both literal names and regex patterns are recognized. Otherwise, you can add a comment before specific selectors:
/* uncss:ignore */
.selector1 {
/* this rule will be ignored */
}
.selector2 {
/* this will NOT be ignored */
}
/* uncss:ignore start */
/* all rules in here will be ignored */
/* uncss:ignore end */
ignoreSheets (string[] | RegExp[]): Do not process these stylesheets, e.g. Google fonts. Accepts strings or regex patterns.
inject (string / function(window)): Path to a local javascript file which is executed before uncss runs. A function can also be passed directly in.
Example inject.js file
'use strict';
module.exports = function(window) {
window.document.querySelector('html').classList.add('no-csscalc', 'csscalc');
};
Example of passing inject as a function
{
inject: function(window){
window.document.querySelector('html').classList.add('no-csscalc', 'csscalc');
}
}
jsdom (object) (Supported only by API): Supply the options used to create the JSDOM pages (https://github.com/jsdom/jsdom). At the moment, config.resources is not yet supported.
media (string[]): By default UnCSS processes only stylesheets with media query _all_, _screen_, and those without one. Specify here which others to include.
raw (string): Give the task a raw string of CSS in addition to the existing stylesheet options; useful in scripting when your CSS hasn't yet been written to disk.
report (boolean, default: true): Return the report object in callback.
strictSSL (boolean, default: true): Disable SSL verification when retrieving html source
stylesheets (string[]): Use these stylesheets instead of those extracted from the HTML files. Prepend paths with the file:// protocol to force use of local stylesheets, otherwise paths will be resolved as a browser would for an anchor tag href on the HTML page.
timeout (number): Specify how long to wait for the JS to be loaded.
uncssrc (string): Load all options from a JSON file. Regular expressions for the ignore and ignoreSheets options should be wrapped in quotation marks.
Example uncssrc file:
{
"ignore": [
".unused",
"/^#js/"
],
"stylesheets": [
"css/override.css"
]
}
userAgent (String, default: 'uncss'): The user agent string that jsdom should send when requesting pages. May be useful when loading markup from services which use user agent based device detection to serve custom markup to mobile devices. Defaults to uncss.
UnCSS can be used as a PostCSS Plugin.
postcss([ require('uncss').postcssPlugin ]);
See PostCSS docs for examples for your environment.
Note: Depending on your environment, you might not be able to use uncss as a PostCSS plugin since the plugin is not directly exported. In such cases, use the wrapper library postcss-uncss.
html (string[]): provide a list of html files to parse for selectors and elements. Usage of globs is allowed.
ignore (string[] | RegExp[]): provide a list of selectors that should not be removed by UnCSS. For example, styles added by user interaction with the page (hover, click), since those are not detectable by UnCSS yet. Both literal names and regex patterns are recognized. Otherwise, you can add a comment before specific selectors in your CSS:
/* uncss:ignore */
.selector1 {
/* this rule will be ignored */
}
.selector2 {
/* this will NOT be ignored */
}
{
html: ['index.html', 'about.html', 'team/*.html'],
ignore: ['.fade']
}
Copyright (c) 2019 Giacomo Martino. See the LICENSE file for license rights and limitations (MIT).