Tailwind CSS v4:到底改了什么,是否该迁移
CSS 优先配置、@layer 集成、内置容器查询、新引擎性能、破坏性变更,以及我从 v3 迁移到 v4 的真实体验。
我从 v1.x 就开始用 Tailwind CSS 了,那时候社区一半人觉得它是个怪胎,另一半人已经靠它疯狂出活了。每个大版本都是一次重大飞跃,但 v4 不一样。它不仅仅是一个功能版本,而是一次从头开始的架构重写,改变了你和框架之间的根本契约。
把两个生产项目从 v3 迁移到 v4,又在 v4 上从零开始了三个新项目之后,我对什么是真正的改进、什么还粗糙、以及你今天是否应该迁移,有了清晰的判断。不吹不黑——只讲我观察到的。
全局视角:v4 到底是什么#
Tailwind CSS v4 同时是三件事:
- 全新引擎 — 从 JavaScript 重写为 Rust(Oxide 引擎),构建速度大幅提升
- 全新配置范式 — CSS 优先配置替代
tailwind.config.js成为默认方式 - 更紧密的 CSS 平台集成 — 原生
@layer、容器查询、@starting-style和级联层成为一等公民
你到处看到的标题是"快 10 倍"。这是真的,但它低估了实际的变化。配置和扩展 Tailwind 的心智模型已经根本性地转变了。你现在是在用 CSS 工作,而不是用一个生成 CSS 的 JavaScript 配置对象。
以下是一个最简的 Tailwind v4 设置:
/* app.css — 这就是全部设置 */
@import "tailwindcss";就这样。没有配置文件。没有 PostCSS 插件配置(对大多数场景而言)。没有 @tailwind base; @tailwind components; @tailwind utilities; 指令。一行导入,就能跑。
对比 v3:
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};// v3 — postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};三个文件精简为一行。这不仅仅是少了样板代码——是减少了配置出错的可能性。在 v4 中,内容检测是自动的,它会自动扫描你的项目文件,无需你拼写 glob 模式。
CSS 优先配置与 @theme#
这是最大的概念转变。在 v3 中,你通过 JavaScript 配置对象自定义 Tailwind:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a5f",
},
},
fontFamily: {
display: ["Inter Variable", "sans-serif"],
},
spacing: {
18: "4.5rem",
112: "28rem",
},
borderRadius: {
"4xl": "2rem",
},
},
},
};在 v4 中,所有这些都用 @theme 指令写在 CSS 里:
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
--font-display: "Inter Variable", sans-serif;
--spacing-18: 4.5rem;
--spacing-112: 28rem;
--radius-4xl: 2rem;
}一开始我是抗拒的。我喜欢有一个 JavaScript 对象能看到整个设计系统。但用了一周 CSS 方式后,我因为三个原因改变了想法:
1. 原生 CSS 自定义属性自动暴露。 你在 @theme 中定义的每个值都成为 :root 上的 CSS 自定义属性。这意味着你的主题值在纯 CSS、CSS Modules、<style> 标签里,任何 CSS 运行的地方都能访问:
/* 这是免费获得的 */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* 任何地方都能用——不需要 Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. 你可以在 @theme 中使用 CSS 特性。 媒体查询、light-dark()、calc()——真正的 CSS 在这里能用,因为它本身就是真正的 CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. 和你的其他 CSS 放在一起。 你的主题、自定义工具类和基础样式全都用同一种语言,想放同一个文件也行。不用在"CSS 世界"和"JavaScript 配置世界"之间来回切换。
覆盖 vs 扩展默认主题#
在 v3 中你有 theme(替换)和 theme.extend(合并)。在 v4 中,心智模型不同:
@import "tailwindcss";
/* 这是扩展默认主题——在现有颜色旁边添加品牌色 */
@theme {
--color-brand-500: #3b82f6;
}如果你想完全替换一个命名空间(比如移除所有默认颜色),用 @theme 配合 --color-* 通配符重置:
@import "tailwindcss";
@theme {
/* 先清除所有默认颜色 */
--color-*: initial;
/* 然后只定义你的颜色 */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}这种通配符重置模式很优雅。你精确选择默认主题的哪些部分保留、哪些替换。想保留所有默认间距但用自定义颜色?重置 --color-*: initial; 然后不动间距就行。
多主题文件#
对于更大的项目,你可以把主题拆分到多个文件:
/* styles/theme/colors.css */
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-200: #bfdbfe;
--color-brand-300: #93c5fd;
--color-brand-400: #60a5fa;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-800: #1e40af;
--color-brand-900: #1e3a5f;
--color-brand-950: #172554;
}
/* styles/theme/typography.css */
@theme {
--font-display: "Inter Variable", sans-serif;
--font-body: "Source Sans 3 Variable", sans-serif;
--font-mono: "JetBrains Mono Variable", monospace;
--text-display: 3.5rem;
--text-display--line-height: 1.1;
--text-display--letter-spacing: -0.02em;
}/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";这比 v3 那种把巨大的 tailwind.config.js 拆分或试图用 require() 的模式干净多了。
Oxide 引擎:真的快了 10 倍#
Tailwind v4 的引擎是用 Rust 完全重写的,他们叫它 Oxide。我对"快 10 倍"的说法持怀疑态度——营销数字很少经得起真实项目的检验。所以我做了基准测试。
我的测试项目: 一个 Next.js 应用,847 个组件,142 个页面,大约 23,000 处 Tailwind 类名使用。
| 指标 | v3 (Node) | v4 (Oxide) | 提升 |
|---|---|---|---|
| 首次构建 | 4,280ms | 387ms | 11x |
| 增量(编辑 1 个文件) | 340ms | 18ms | 19x |
| 全量重建(清理后) | 5,100ms | 510ms | 10x |
| 开发服务器启动 | 3,200ms | 290ms | 11x |
对我的项目来说,"10 倍"的说法是保守的。增量构建才是真正闪光的地方——18ms 意味着基本上是即时的。你保存文件,浏览器在你切换标签页之前就已经有了新样式。
为什么这么快?#
三个原因:
1. Rust 代替 JavaScript。 核心 CSS 解析器、类名检测和代码生成全是原生 Rust。这不是"为了好玩重写成 Rust"的情况——CSS 解析确实是 CPU 密集型工作,原生代码相比 V8 有巨大优势。
2. 热路径不经过 PostCSS。 在 v3 中,Tailwind 是一个 PostCSS 插件。每次构建意味着:将 CSS 解析为 PostCSS AST,运行 Tailwind 插件,序列化回 CSS 字符串,然后其他 PostCSS 插件运行。在 v4 中,Tailwind 有自己的 CSS 解析器,直接从源码到输出。PostCSS 仍然支持兼容性,但主路径完全跳过它。
3. 更智能的增量处理。 新引擎积极缓存。当你编辑单个文件时,它只重新扫描该文件的类名,只重新生成变化的 CSS 规则。v3 引擎在这方面比人们认为的更聪明(JIT 模式已经是增量的了),但 v4 通过细粒度的依赖追踪走得更远。
速度真的重要吗?#
重要,但不是你想的那个原因。对大多数项目来说,v3 的构建速度"还行"。开发时等几百毫秒,不痛苦。
v4 的速度之所以重要,是因为它让 Tailwind 在你的工具链中变得隐形。当构建在 20ms 以内时,你不再把 Tailwind 当作一个构建步骤来思考了。它变得像语法高亮一样——永远在那里,从不碍事。在一整天的开发中,这种心理差异是很显著的。
原生 @layer 集成#
在 v3 中,Tailwind 使用自己的层系统,有 @layer base、@layer components 和 @layer utilities。它们看起来像 CSS 级联层但不是——它们是 Tailwind 特有的指令,控制生成的 CSS 出现在输出中的位置。
在 v4 中,Tailwind 使用真正的 CSS 级联层:
/* v4 输出 — 简化版 */
@layer theme, base, components, utilities;
@layer base {
/* 重置,preflight */
}
@layer components {
/* 你的组件类 */
}
@layer utilities {
/* 所有生成的工具类 */
}这是一个重大变化,因为 CSS 级联层有真正的优先级影响。低优先级层中的规则永远输给高优先级层中的规则,无论选择器特异性如何。这意味着:
@layer components {
/* 特异性:0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* 特异性:0-1-0 — 相同特异性但胜出,因为 utilities 层在后面 */
.p-4 { padding: 1rem; }
}工具类永远覆盖组件。组件永远覆盖基础。这在概念上和 v3 中 Tailwind 的工作方式一样,但现在它是由浏览器的级联层机制强制执行的,而不是通过源码顺序操作。
添加自定义工具类#
在 v3 中,你通过插件 API 或 @layer utilities 定义自定义工具类:
// v3 — 插件方式
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};在 v4 中,自定义工具类用 @utility 指令定义:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}@utility 指令告诉 Tailwind"这是一个工具类——把它放在 utilities 层中,允许它和变体一起使用。"最后这点是关键。用 @utility 定义的工具类自动支持 hover:、focus:、md: 和所有其他变体:
<p class="text-pretty md:text-balance">...</p>自定义变体#
你也可以用 @variant 定义自定义变体:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Click me
</button>这替代了 v3 的 addVariant 插件 API 在大多数场景下的使用。它不那么强大(你无法做编程式的变体生成),但覆盖了人们实际使用的 90%。
容器查询:内置,无需插件#
容器查询是 v3 中最受请求的功能之一。你可以通过 @tailwindcss/container-queries 插件获得,但它是附加的。在 v4 中,它们内置于框架。
基本用法#
用 @container 标记一个容器,用 @ 前缀查询其大小:
<!-- 标记父元素为容器 -->
<div class="@container">
<!-- 相对于父元素宽度响应,不是视口 -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">卡片 1</div>
<div class="p-4">卡片 2</div>
<div class="p-4">卡片 3</div>
</div>
</div>@md、@lg 等变体的工作方式类似响应式断点,但是相对于最近的 @container 祖先而不是视口。断点值对应 Tailwind 的默认断点:
| 变体 | 最小宽度 |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
命名容器#
你可以给容器命名以查询特定祖先:
<div class="@container/sidebar">
<div class="@container/card">
<!-- 查询 card 容器 -->
<div class="@md/card:text-lg">...</div>
<!-- 查询 sidebar 容器 -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>为什么这很重要#
容器查询改变了你对响应式设计的思考方式。不再是"在这个视口宽度下,显示三列",而是"当这个组件的容器足够宽时,显示三列"。组件变得真正自包含。你可以把一个卡片组件从全宽布局移到侧边栏,它会自动适应。不需要媒体查询的花式操作。
我一直在重构我的组件库,默认使用容器查询而不是视口断点。结果是组件放在任何地方都能工作,父组件不需要知道组件的响应式行为。
<!-- 这个组件适应任何放置它的容器 -->
<article class="@container">
<div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
<img
class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
src="/post-image.jpg"
alt=""
/>
<div>
<h2 class="text-lg @lg:text-xl font-semibold">文章标题</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
文章摘要放这里……
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">标签</span>
</div>
</div>
</div>
</article>真正有用的新变体#
v4 添加了几个我一直在用的新变体。它们填补了真实的空白。
starting: 变体#
这映射到 CSS @starting-style,让你定义元素首次出现时的初始状态。这是不用 JavaScript 就能实现元素入场动画的缺失拼图:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>这个对话框打开时会淡入</p>
</dialog>starting: 变体生成 @starting-style 块内的 CSS:
/* Tailwind 生成的内容 */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}这对对话框、弹出框、下拉菜单来说意义重大——任何需要入场动画的东西。以前,你需要用 JavaScript 在下一帧添加一个类,或者用 @keyframes。现在只需一个工具类。
not-* 变体#
取反。我们一直想要的东西:
<!-- 除了最后一个子元素外,每个都有边框 -->
<div class="divide-y">
<div class="not-last:pb-4">项目 1</div>
<div class="not-last:pb-4">项目 2</div>
<div class="not-last:pb-4">项目 3</div>
</div>
<!-- 给所有非 disabled 的元素添加样式 -->
<input class="not-disabled:hover:border-brand-500" />
<!-- 取反 data 属性 -->
<div class="not-data-active:opacity-50">...</div>nth-* 变体#
直接访问 nth-child 和 nth-of-type:
<ul>
<li class="nth-1:font-bold">第一项——加粗</li>
<li class="nth-even:bg-gray-50">偶数行——灰色背景</li>
<li class="nth-odd:bg-white">奇数行——白色背景</li>
<li class="nth-[3n+1]:text-brand-500">每三个加一——品牌色</li>
</ul>方括号语法(nth-[3n+1])支持任何有效的 nth-child 表达式。这替代了我过去为表格条纹和网格模式写的大量自定义 CSS。
in-* 变体(父状态)#
这是 group-* 的反向。不是"当我的父元素(group)被悬停时,给我添加样式",而是"当我在一个匹配此状态的父元素内部时,给我添加样式":
<div class="in-data-active:bg-brand-50">
当任何祖先有 data-active 时获得背景色
</div>**: 深层通用变体#
给所有后代添加样式,不仅仅是直接子元素。这是受控的力量——谨慎使用,但对散文内容和 CMS 输出来说非常有价值:
<!-- 这个 div 内所有层级的段落 -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>这个会被高亮</p>
<div>
<p data-highlight>这个也是,嵌套更深</p>
</div>
</section>
</div>破坏性变更:实际上什么崩了#
我直说。如果你有一个大型 v3 项目,迁移不是轻而易举的事。以下是我的项目中出问题的地方:
1. 配置格式#
你的 tailwind.config.js 不能直接使用。你需要:
- 把它转换为
@themeCSS(推荐,适配新架构) - 使用兼容层
@config指令(快速迁移路径)
/* 快速迁移——保留你的旧配置 */
@import "tailwindcss";
@config "../../tailwind.config.js";这个 @config 桥梁能用,但它明确是一个迁移工具。建议是随着时间推移逐步迁移到 @theme。
2. 移除了废弃的工具类#
一些在 v3 中被废弃的工具类已经消失:
/* v4 中已移除 */
bg-opacity-* → 使用 bg-black/50(斜杠透明度语法)
text-opacity-* → 使用 text-black/50
border-opacity-* → 使用 border-black/50
flex-shrink-* → 使用 shrink-*
flex-grow-* → 使用 grow-*
overflow-ellipsis → 使用 text-ellipsis
decoration-slice → 使用 box-decoration-slice
decoration-clone → 使用 box-decoration-clone
如果你在 v3 中已经使用了现代语法(斜杠透明度、shrink-*),那就没问题。否则,这些都是简单的查找替换。
3. 默认调色板变化#
默认调色板略有调整。如果你依赖 v3 中精确的颜色值(不是按名称而是实际的十六进制值),你可能会注意到视觉差异。命名颜色(blue-500、gray-200)仍然存在,但一些十六进制值变了。
4. 内容检测#
v3 需要显式的 content 配置:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 使用自动内容检测。它扫描你的项目根目录并自动找到模板文件。这大多数情况下"直接能用",但如果你的项目结构不寻常(monorepo 中有项目根目录外的包、模板文件在意外位置),你可能需要显式配置源路径:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. 插件 API 变化#
如果你写过自定义插件,API 变了。addUtilities、addComponents、addBase 和 addVariant 函数通过兼容层仍然能用,但 v4 的惯用方式是 CSS 原生:
// v3 插件
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — 纯 CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}大多数官方插件(@tailwindcss/typography、@tailwindcss/forms、@tailwindcss/aspect-ratio)都有 v4 兼容版本。第三方插件就不一定了——迁移前检查它们的仓库。
6. JIT 是唯一模式#
在 v3 中,你可以选择不使用 JIT 模式(虽然几乎没人这样做)。在 v4 中,没有非 JIT 模式。一切都是按需生成的,始终如此。如果你有某种理由使用旧的 AOT(提前编译)引擎,那条路已经没了。
7. 一些变体语法变化#
少数变体被重命名或改变了行为:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — >* 部分现在使用 inset 变体语法 -->
<div class="*:p-4">...</div>任意变体语法 [&...] 仍然有效,但 v4 为常见模式提供了命名替代方案。
迁移指南:真实过程#
以下是我实际的迁移过程,不是文档中的快乐路径,而是真实的样子。
第一步:运行官方 codemod#
Tailwind 提供了一个 codemod 处理大部分机械性变更:
npx @tailwindcss/upgrade它自动做了很多事:
- 将
@tailwind指令转换为@import "tailwindcss" - 重命名废弃的工具类
- 更新变体语法
- 将透明度工具类转换为斜杠语法(
bg-opacity-50到bg-black/50) - 从你的配置创建基础的
@theme块
Codemod 处理得好的#
- 工具类重命名(近乎完美)
- 指令语法变化
- 简单的主题值(颜色、间距、字体)
- 透明度语法迁移
Codemod 处理不了的#
- 复杂的插件转换
- 动态配置值(JavaScript 中的
theme()调用) - 条件主题配置(如基于环境的主题值)
- 自定义插件 API 迁移
- 新解析器解释方式不同的任意值边界情况
- JavaScript 中动态构建的类名(模板字面量、字符串拼接)
第二步:修复 PostCSS 配置#
对大多数设置,你需要更新 PostCSS 配置:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};注意:插件名从 tailwindcss 改为 @tailwindcss/postcss。如果你使用 Vite,可以完全跳过 PostCSS,使用 Vite 插件:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});第三步:转换主题配置#
这是手动部分。把你的 tailwind.config.js 主题值转换为 @theme:
// v3 配置 — 迁移前
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: "#60a5fa",
DEFAULT: "#3b82f6",
dark: "#1d4ed8",
},
},
fontSize: {
"2xs": ["0.65rem", { lineHeight: "1rem" }],
},
animation: {
"fade-in": "fade-in 0.5s ease-out",
},
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
},
},
},
};/* v4 CSS — 迁移后 */
@import "tailwindcss";
@theme {
--color-brand-light: #60a5fa;
--color-brand: #3b82f6;
--color-brand-dark: #1d4ed8;
--text-2xs: 0.65rem;
--text-2xs--line-height: 1rem;
--animate-fade-in: fade-in 0.5s ease-out;
}
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}注意 keyframes 从 @theme 中移出来变成了普通 CSS @keyframes。@theme 中的动画名只是引用它们。这更干净——keyframes 是 CSS,就应该写成 CSS。
第四步:视觉回归测试#
这不可商量。迁移后,我打开了应用的每个页面进行目视检查。我也运行了 Playwright 截图测试(如果你有的话)。Codemod 很好但不完美。我在目视检查中发现的问题:
- 一些透明度语法迁移产生了略微不同结果的地方
- 没有延续过来的自定义插件输出
- 由于层排序导致的 z-index 堆叠变化
- 一些
!important覆盖在级联层下行为不同
第五步:更新第三方依赖#
检查每个 Tailwind 相关的包:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "移除——现在内置了",
"tailwindcss-animate": "检查 v4 支持",
"prettier-plugin-tailwindcss": "更新到最新"
}@tailwindcss/container-queries 插件不再需要——容器查询已内置。其他插件需要它们的 v4 兼容版本。
配合 Next.js 使用#
因为我大多数项目用 Next.js,这里是具体的设置。
PostCSS 方式(Next.js 推荐)#
Next.js 底层使用 PostCSS,所以 PostCSS 插件是自然的选择:
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};/* app/globals.css */
@import "tailwindcss";
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}这就是完整的设置。不需要 tailwind.config.js,不需要 autoprefixer(v4 内部处理浏览器前缀)。
CSS 导入顺序#
有一个坑让我踩了:由于级联层的存在,v4 中 CSS 导入顺序更重要了。你的 @import "tailwindcss" 应该在自定义样式之前:
/* 正确顺序 */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* 你的内联 @theme、@utility 等 */如果你在 Tailwind 之前导入自定义 CSS,你的样式可能会在更低的级联层中,被意外覆盖。
深色模式#
深色模式概念上的工作方式不变,但配置移到了 CSS:
@import "tailwindcss";
/* 使用基于类的深色模式(默认是基于媒体查询的) */
@variant dark (&:where(.dark, .dark *));这替代了 v3 配置:
// v3
module.exports = {
darkMode: "class",
};@variant 方式更灵活。你可以随心所欲地定义深色模式——基于类、基于 data 属性或基于媒体查询:
/* data 属性方式 */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* 媒体查询——这是默认的,所以你不需要声明 */
@variant dark (@media (prefers-color-scheme: dark));Turbopack 兼容性#
如果你使用 Next.js 配合 Turbopack(现在是默认的开发打包器),v4 运行得很好。Rust 引擎和 Turbopack 自身基于 Rust 的架构配合得很好。我测量了开发启动时间:
| 设置 | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| 冷启动 | 4.8s | 2.1s | 1.3s |
| HMR(CSS 变更) | 450ms | 180ms | 40ms |
CSS 变更 40ms 的 HMR 几乎感知不到。感觉就是即时的。
性能深入:构建速度之外#
Oxide 引擎的好处不仅仅是原始构建速度。
内存使用#
v4 使用的内存显著减少。在我那个 847 组件的项目上:
| 指标 | v3 | v4 |
|---|---|---|
| 峰值内存(构建) | 380MB | 45MB |
| 稳态(开发) | 210MB | 28MB |
这对内存受限的 CI/CD 流水线很重要,对同时运行十个进程的开发机器也很重要。
CSS 输出大小#
v4 生成的 CSS 输出略小,因为新引擎在去重和死代码消除方面做得更好:
v3 输出:34.2 KB(gzip 后)
v4 输出:29.8 KB(gzip 后)
不改任何代码就减少了 13%。不是颠覆性的,但是免费的性能提升。
主题值的 Tree Shaking#
在 v4 中,如果你定义了一个主题值但从未在模板中使用它,对应的 CSS 自定义属性仍然会被输出(它在 @theme 中,映射到 :root 变量)。但是,未使用值的工具类不会被生成。这和 v3 的 JIT 行为相同,但值得注意:你的 CSS 自定义属性始终可用,即使没有工具类使用它们。
如果你想阻止某些主题值生成 CSS 自定义属性,可以使用 @theme inline:
@theme inline {
/* 这些值生成工具类但不生成 CSS 自定义属性 */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}这对你不想暴露为 CSS 变量的内部设计令牌很有用。
高级用法:多品牌主题组合#
v4 让多品牌主题显著变得更容易的一个模式。因为主题值是 CSS 自定义属性,你可以在运行时替换它们:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* 品牌覆盖 */
.theme-acme {
--brand-primary: #e11d48;
--brand-light: #fb7185;
--brand-dark: #9f1239;
}
.theme-globex {
--brand-primary: #059669;
--brand-light: #34d399;
--brand-dark: #047857;
}<body class="theme-acme">
<!-- 所有 bg-brand、text-brand 等都使用 Acme 的颜色 -->
<div class="bg-brand text-white">Acme Corp</div>
</body>在 v3 中,这需要一个自定义插件或在 Tailwind 外面做复杂的 CSS 变量设置。在 v4 中,这很自然——主题就是 CSS 变量,CSS 变量会级联。这就是 CSS 优先方式让人觉得正确的那种东西。
我怀念 v3 的什么#
让我客观一些。v3 有些东西我在 v4 中确实怀念:
1. 用于编程式主题的 JavaScript 配置。 我有个项目,在配置中用 JavaScript 函数从单一品牌色生成整个色阶。在 v4 中,你无法在 @theme 里这样做——你需要一个生成 CSS 文件的构建步骤,或者一次性计算好颜色然后粘贴进去。@config 兼容层有所帮助,但这不是长期方案。
2. 刚发布时 IntelliSense 更好。 v3 的 VS Code 扩展经过了多年打磨。v4 的 IntelliSense 能用,但早期有一些不足——自定义 @theme 值有时无法自动补全,@utility 定义也不总是被识别。最近的更新已经大幅改善了,但值得一提。
3. 生态成熟度。 v3 周围的生态系统非常庞大。Headless UI、Radix、shadcn/ui、Flowbite、DaisyUI——一切都是针对 v3 测试的。v4 支持正在陆续推出但还不是普遍的。我不得不给一个组件库提了 PR 来修复 v4 兼容性。
你应该迁移吗?#
以下是我在用了 v4 好几周后的决策框架:
现在就迁移,如果:#
- 你在开始一个新项目(显而易见的选择——直接用 v4)
- 你的项目几乎没有自定义插件
- 你想在大型项目中获得性能优势
- 你已经在使用现代 Tailwind 模式(斜杠透明度、
shrink-*等) - 你需要容器查询但不想额外装插件
等一等,如果:#
- 你严重依赖尚未支持 v4 的第三方 Tailwind 插件
- 你有复杂的编程式主题配置
- 你的项目很稳定,不在积极开发中(何必动它?)
- 你正在功能冲刺中(在冲刺间迁移,不要在冲刺中)
不要迁移,如果:#
- 你还在 v2 或更早版本(先升级到 v3,稳定后再考虑 v4)
- 你的项目几个月后就结束了(不值得折腾)
我的真实看法#
对于新项目,v4 是毫无疑问的选择。CSS 优先配置更干净,引擎快得多,新功能(容器查询、@starting-style、新变体)确实有用。
对于现有项目,我建议分阶段:
- 现在: 新项目全部用 v4 开始
- 很快: 尝试把一个小型内部项目转换到 v4
- 准备好后: 在一个安静的冲刺期间迁移生产项目,配合视觉回归测试
如果你有所准备,迁移并不痛苦。Codemod 处理了 80% 的工作。剩下的 20% 是手动但直接的。中型项目预留一天,大型项目两到三天。
Tailwind v4 是 Tailwind 本应有的样子。JavaScript 配置一直是对当时工具的妥协。CSS 优先配置、原生级联层、Rust 引擎——这些不是潮流,是框架在追赶平台。Web 平台变得更好了,Tailwind v4 选择顺应它而不是对抗它。
把你的设计令牌写在 CSS 中,用 CSS 特性组合它们,让浏览器自己的级联处理优先级——这是正确的方向。走了四个大版本才到这里,但结果是迄今为止最一致的 Tailwind 版本。
下个项目就用它吧。你不会想回头的。