栏目:01 · 前端工程化(1 月 · 前端基础与 Vue3 入门) 环境基线:UnoCSS 66.4.0 / Tailwind CSS 4.1.11 / Vite 7.1.4 / Vue 3.5.18 / Node 22.11.0
上个月给公司内部运维平台起新项目,选型会上同事问了我一句:原子 CSS 用不用?用 Tailwind 还是 UnoCSS?我当场给了答案,一周后又自己改了口。这篇把两次判断的依据都摊开,包括我之前理解错的部分——先说结论放前面可能就更像 AI 了,所以按我实际的心路来。
我一开始的理解就是错的
2023 年我在老项目里用过一年 Tailwind 3,后来嫌它 dev 模式下中等规模项目改一次类名要等 600ms 上下(当时 JIT 扫描全量文件),切到了 UnoCSS。所以很长一段时间里我对这俩的认知是:UnoCSS 就是 Tailwind 的性能优化复刻版,类名几乎一样,选它纯粹为了快。
这个理解在 Tailwind 4 发布之后基本作废了。
Tailwind 4.0(2025 年 1 月发的,我项目现在固定用 4.1.11)把扫描引擎换成了 Rust 写的 Oxide,构建管线走 Lightning CSS。同一个项目我实测过:全量 build 从 3.4 时代的 8 秒左右降到 1.3 秒,dev 增量更新进到了几十毫秒级。UnoCSS 在 dev 端确实还是更快一点(它连"构建"都谈不上,就是正则匹配按需产出),但这个差距已经缩小到人的感知边缘。拿速度当选 UnoCSS 的理由,2026 年已经不成立了。
后来我重新想了想自己为什么实际用得很舒服,才发现真正的原因在另一个地方,这也是我想说的认知反转。
反转:UnoCSS 不是 CSS 框架,是引擎
Tailwind 是一个"CSS 框架"——官方提供一套固定的工具类体系,你在这套体系里写。而 UnoCSS 官方给自己的定位是 atomic CSS 引擎:它只负责"匹配类名 → 产出 CSS"这一层,预设(preset)才决定你写的是什么语法。
这件事带来的实际差异,我列三个真用上的:
一是图标即类名。 presetIcons 让任何 Iconify 图集里的图标直接当类名写:
<i class="i-tabler-settings" />
<i class="i-logos-vue" />
我以前的做法是去官网复制 SVG 粘到组件里,一个中等项目能攒出几十个内联 SVG,改颜色要去翻 fill 属性。现在图标和文本类一样走 currentColor,text-red-500 直接给它上色。
二是 shortcuts 写组件级抽象。
// uno.config.ts(unocss 66.4.0)
import { defineConfig, presetWind4, presetIcons,
transformerVariantGroup, transformerDirectives } from 'unocss'
export default defineConfig({
presets: [
presetWind4(), // Tailwind 兼容预设,66.x 起推荐用它替代旧的 presetUno
presetIcons({ scale: 1.2, extraProperties: { display: 'inline-block' } }),
],
transformers: [transformerVariantGroup(), transformerDirectives()],
shortcuts: {
btn: 'inline-flex items-center justify-center h-9 px-4 rounded-md text-sm font-medium transition-colors',
'btn-primary': 'btn bg-brand-500 text-white hover:bg-brand-600',
},
})
btn-primary 复用 btn,一个团队自己的"迷你组件库"就这么长出来了,不用发 npm 包。Tailwind 4 里等价物是 @utility 指令,能做但表达力弱一截,@utility 里不能直接引用另一个 utility 组合(截至 4.1.11 是这样,这是我目前最不满意它的一点)。
三是规则可自写。 项目里有个历史遗留约定"凡是危险操作按钮必有抖动动画",在 UnoCSS 里一条 rule 十行搞定,产出 animate-shake 这种自定义类。Tailwind 4 要写 CSS 插件,成本高一个量级。
反过来说,Tailwind 4 也有 UnoCSS 给不了的东西:@theme 里定义的 design token 会自动暴露成 CSS 变量给整个项目用,官方生态(daisyUI、shadcn/ui 的 Tailwind 版)都是围绕它的。这个生态位 UnoCSS 靠 presetWind4 兼容层去蹭,能蹭到大部分,但不是原生。
踩坑:动态类名拼接,静默失败
两套方案共享的头号大坑,但 UnoCSS 那次坑得我更狠。权限徽章组件里我写了这个:
<span :class="`text-${level}-500`">{{ role.name }}</span>
页面上 class 属性明明有 text-red-500,样式就是不出来,控制台无报错、终端无警告。查了半小时才反应过来:UnoCSS 的扫描器是对源码文本做正则提取,它能看见的是源文件里出现过的字符串字面量,text-${level}-500 这种运行时拼出来的东西它永远匹配不到。构建产物里验证一下:
$ grep -o "text-red-500" dist/assets/index-*.css | wc -l
0
什么都没有。没有报错是这类问题最恶心的地方——它不报错,就是安静地不给样式。
解法是把完整类名写在 safelist 里:
// uno.config.ts
safelist: ['text-red-500', 'text-green-500', 'text-amber-500'],
Tailwind 4 的等价物是 CSS 里的 @source inline("text-{red,green,amber}-500")。我的习惯后来统一成了:动态样式一律用内联 style 或 CSS 变量切换,类名拼接直接在 code review 里禁掉,比事后 safelist 省心。
踩坑:Tailwind 4 升级,暗色模式静默失效
第二个坑来自给老项目升 Tailwind 4。升完暗色模式整个失效——点切换按钮没反应,也没有任何报错。原因是我还留着 3 时代的 tailwind.config.js,里面写着 darkMode: 'class'。
Tailwind 4 默认不读 tailwind.config.js(想读要显式 @config 指令声明),暗色模式的类名策略改成了在 CSS 里声明:
/* app.css(Tailwind CSS 4.1.11) */
@import "tailwindcss";
@theme {
–color-brand-500: oklch(0.62 0.19 45);
}
/* v3 的 darkMode:'class',v4 的等价写法 */
@custom-variant dark (&:where(.dark, .dark *));
这行不写,dark: 变体就走默认的 prefers-color-scheme 媒体查询,和你的手动切换逻辑完全对不上。我给这个坑交的学费是一下午,而它的一切症状都指向"JS 逻辑写错了",方向完全找偏。
顺带一个硬性约束:Tailwind 4 要求 Chrome 111+ / Safari 16.4+,因为底层用了 @property 和 color-mix()。toB 内部系统无所谓,做 toC 面向老设备的业务要掂量。UnoCSS 产出的 CSS 兼容性跟目标浏览器走,约束更松。
踩坑:presetIcons 忘装图集
第三个坑最便宜但最频繁,新同事基本必踩一次。presetIcons 找不到图集包时,dev server 直接起不来:
[vite] Internal server error: Failed to resolve import "@iconify-json/tabler" from "uno.config.ts".
Does the file exist?
缺什么装什么:pnpm i -D @iconify-json/tabler。建议项目模板里把常用图集预置进 devDependencies,一劳永逸。
数据与选择
最后给点数字背景,都是同一台机器(i5-12400,NVMe)上实测,项目规模约 180 个 Vue SFC:旧项目手写 SCSS + BEM 时产物 CSS 214KB,迁到 UnoCSS 后 31KB,且这个数字基本不随页面数增长——按需生成,页面不用到的类压根不存在。dev 端 UnoCSS 增量更新在 10ms 量级,Tailwind 4 在 30-50ms 量级,感知差异不大;全量 build Tailwind 4 略快,但两者都在 2 秒内,不再是决策因素。
所以现在我的推荐是:要生态成熟、团队人数多、需要现成组件库方案的,Tailwind 4,别用 3,4 的速度和 CSS-first 配置是代差级提升;一人或小团队、重度依赖图标和自定义规则、想极致控制产物体积的,UnoCSS。 我个人新项目默认 UnoCSS——不是它全面更好,而是 presetIcons 和 shortcuts 对我的工作流价值太大,我愿意为这个付"生态靠兼容层"的代价。你要是主要在 React/Nuxt 社区里混,跟 Tailwind 走省心得多,这个场景我不替它吹。
另外提一句 attributify 预设(把 text-center 写成 text="center"),我试用过两周放弃了:IDE 提示支持弱,新人看代码第一反应是写错了。炫技有余,工程价值存疑。
还有一个没解决的问题挂在这:UnoCSS 的 shortcuts 产出的类,在被压缩的构建产物里偶发和按需提取的顺序打架,同一个组合类两种场景下优先级不一致,我目前的 workaround 是重要组件还是老老实实写真类名。如果你知道更干净的解法,评论区求指路。

