旧项目用purgecss压缩css体积失败主因是未处理动态类名,因其仅静态分析、不解析运行时拼接,必须用safelist显式保留;常见动态来源包括模板字符串、js直接操作和响应式绑定,白名单应优先精确匹配或可控模式如"md:text-*"、正则 /^(?:sm|md|lg):text-(?:red|blue)-(?:500|600)$/。

直接说结论:旧项目用 PurgeCSS 压缩 CSS 体积,80% 的失败不是配置错,而是没处理动态类名——它根本不会“猜”你运行时拼出来的 btn-primary 或 text-sm,必须靠 safelist 显式兜底。
为什么旧项目一上 PurgeCSS 就样式崩塌
旧项目常见三类动态类来源,PurgeCSS 默认全忽略:
-
className={`icon-${type}`}这种模板字符串,提取器只看到`icon-${,后面全丢 -
el.classList.add('hidden')这类 JS 直接操作,不在 HTML/JSX 字符串里静态出现 -
v-bind:class="['btn', isPrimary && 'btn-primary'](Vue)或className={clsx('text', size)}(React),AST 解析器不介入,只扫纯文本
结果就是:构建后 text-red-500 消失、md:hidden 不生效、第三方组件的 ant-btn-primary 全没了。这不是 bug,是设计使然。
白名单怎么写才真正起作用
别用宽泛正则,优先选精确匹配或可控模式。以下写法按推荐度排序:
- 完整类名直写:
safelist: ['is-active', 'nav-open', 'sr-only']—— 适合全局固定类 - glob 通配响应式前缀:
"md:text-*"、"hover:opacity-*"—— Tailwind 用户最省事 - 正则限定组合范围:
{ pattern: /^(?:sm|md|lg):text-(?:red|blue)-(?:500|600)$/ }—— 避免/text-.*/这种误保text-clip - 函数式过滤(高级):
{ pattern: /btn-(primary|secondary|danger)/ }—— 精准覆盖按钮变体
注意:safelist 优先级最高,哪怕 content 路径漏扫,这些类也一定保留。
content 扫描路径常被忽略的坑
旧项目往往混用多种技术栈,光写 ./src/**/*.{js,jsx,ts,vue} 不够:
- PHP 模板(
.php)必须显式加入:content: ['./src/**/*.{php,js,jsx,ts,vue}'] - Vue 单文件组件中
<div :class="xxx"> 的绑定,需确认插件是否启用 <code>extractors支持 Vue SFC(如postcss-purgecss需配defaultExtractor) - Vite 项目里
import.meta.glob动态导入的模块,其内部字符串不会被自动扫描,得手动加进content数组 - 第三方库传入的 class(如
<mytable class="custom-table"></mytable>),若来自 props 或 JSON 配置,只能靠safelist保 - 在
tailwind.config.js加debug: true(v3.3+),构建日志会输出preserved和discarded类名列表 - 临时注释掉
content字段再 build 一次——如果样式全回来,100% 是 purge 误删,不是 CSS 写错 - 用 CLI 单独测试:
npx tailwindcss -i ./src/input.css -o ./dist/output.css --minify --content "./src/**/*.{js,ts,jsx,tsx}",绕过构建工具封装干扰
验证删了哪些类,别靠眼睛猜
构建后样式异常,第一件事不是改配置,而是看 PurgeCSS 到底干了什么:
真正容易被忽略的是:旧项目里那些藏在 data-class、data-theme 或内联 style 属性里的类名逻辑,它们既不进 content 扫描,也不在 safelist 里——要么改代码显式写出,要么加 extractor 提取。











