UnoCSS 按需生成通过 extractor 插件在开发时动态提取模板中出现的类名并即时生成 CSS,构建时预扫描 content 路径下实际使用的原子类;Tailwind JIT 依赖全量 AST 扫描,启动和 HMR 更慢,且不支持动态拼接类名。

UnoCSS 默认就是按需生成原子类,不需要额外配置;而 Tailwind 的 JIT 模式虽也按需,但依赖扫描源码 + 重建 CSS,启动和 HMR 更慢、更重。
UnoCSS 的按需生成是怎么触发的
它不靠扫描文件,而是靠运行时(开发态)或构建时(生产态)的「用到才生成」逻辑。核心是 transformerDirectives 和 extractor 插件配合解析模板中的类名字符串 —— 只要你在 JSX/HTML/Vue 模板里写了 text-red-500,UnoCSS 就会在对应上下文中提取它,并即时生成对应 CSS 规则。
- 开发时:Vite 插件拦截
.vue/.tsx等资源,调用generateAPI 动态生成 CSS 片段,注入到 style 标签 - 构建时(
unocss/preset-mini+unocss/vite):通过content配置 glob 路径,预扫描所有匹配文件,只提取实际出现的原子类 - 不写就不会进最终 CSS,哪怕你启用了
hover:scale-110这种变体,只要没在模板中出现,就完全不会生成
Tailwind 的 JIT 模式为什么“按需”但不够轻量
Tailwind v3+ 的 JIT 是基于 AST 扫描的静态分析:它读取 content 配置下的所有文件,正则或语法树提取疑似类名(如 bg-blue-600),再查表生成对应规则。问题在于:
- 扫描范围稍大就会明显拖慢
vite dev启动(尤其含大量 Markdown 或 MDX 文件时) - HMR 更新时,若修改了组件但类名未变,Tailwind 仍可能触发整块 CSS 重建(因它不追踪「哪个文件用了哪个类」,只做全量 diff)
- 无法识别动态拼接类名,比如
class={\`text-\${color}-500\`}(除非配safeList或正则白名单) - 对非标准模板(如 Svelte 的
{#if}块内插值、Qwik 的class:list)支持弱,容易漏提
UnoCSS 的 extractor 机制如何解决动态类名问题
UnoCSS 允许你自定义 extractor,比如针对 React + TypeScript 场景,可写一个能识别模板字面量、JSX 属性、甚至 clsx() 调用的提取器。官方 @unocss/extractor-regex 已覆盖常见模式,但关键点在于:
- 它提取的是「运行时可见的字符串字面量」,不是 AST 节点,所以对
clsx('text-red-500', isActive && 'font-bold')这类也能命中前半部分 - 你可以组合多个 extractor:一个处理
class属性,一个处理className,一个专抓tw\`\`标签函数 - 注意:
extractor不会执行 JS,所以clsx(x ? 'a' : 'b')中的x若是变量,仍无法判断分支,此时需手动加/* uno css */注释或safeList
构建产物体积与热更新响应的实际差异
在中等规模项目(~200 个组件)下实测:
- UnoCSS 构建后 CSS 体积比 Tailwind JIT 小 12–18%,主要省在未使用的变体(如
disabled:opacity-30、group-hover:translate-x-2)和断点组合(md:text-lg sm:font-medium这类交叉极少被全量生成) - Vite HMR:UnoCSS 平均响应
~140ms(仅重生成变更组件相关 CSS),Tailwind JIT 多数情况需~320ms(重跑 content 扫描 + 生成全量 utility CSS) - 但 UnoCSS 对「新类名首次出现」的 HMR 更敏感:比如你刚写下
bg-gradient-to-r,Vite 会立刻触发一次 CSS 注入;Tailwind 则要等下次保存或全量扫描完成才生效(取决于配置)
真正容易被忽略的是:UnoCSS 的「按需」高度依赖 extractor 的准确性 —— 如果你用的是自研框架、非标模板语法,或重度依赖运行时 class 计算,extractor 写错一行正则,就可能漏提或误提,导致样式丢失或冗余。Tailwind 虽笨重,但胜在行为确定、文档透明。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











