去年初我们团队接手了一个运行了三年的电商后台系统,当时它还在用 Tailwind CSS v2,打包出来的 CSS 文件足足有 1.2MB,首屏加载光样式就要解析 300 多毫秒。我盯着构建日志里的 tailwindcss 步骤,每次跑开发环境都要等 4 秒多,实在忍不了,就趁着 v4.0 刚发布(2024 年 3 月那波更新)直接升了级。
v4 的 JIT 编译器跟之前完全不是一个量级。我专门做了个对比测试:拿我们后台的订单列表页当样本,这个页面有 1200 多个 DOM 节点,之前用 v2 的时候,JIT 虽然已经比旧版按需生成好很多,但每次改个 p-4 之类的类,热更新还是要等 1.2 秒左右。升级到 v4.0.3 之后,同样的改动,热更新直接降到 180ms 以内。我查了下官方更新日志,v4 重写了 JIT 的 AST 解析逻辑,现在扫描 HTML/JSX 里的工具类时,会跳过已经被缓存的节点,而不是全量重新解析。
更夸张的是生产环境的 CSS 体积。我们之前为了兼容一些动态生成的类(比如根据后端返回的 status 字段动态拼 bg-green-500 之类的),在 tailwind.config.js 里开了 safelist,把 200 多个可能用到的类都列进去了,结果 v2 生成的 CSS 还是有 870KB。v4 的 JIT 现在支持动态类 [w-120px] 这种格式的预扫描,我直接把 safelist 清掉了 80%,最后生产包里的 CSS 只有 92KB,gzip 之后才 28KB。我们线上 CDN 的 QPS 峰值到 12 万的时候,样式文件的加载时间从平均 210ms 降到了 45ms,这个提升对移动端用户感知特别明显。
我遇到过一个实际问题:升级 v4 之后,之前写的 w-[calc(100%-20px)] 突然不生效了。排查了半天,发现 v4 的 JIT 对动态值里的特殊字符处理更严格了,之前的写法是 w-[calc(100% - 20px)](中间有空格),v4 的解析器会把空格当成类名分隔符,直接把 calc(100% 当成无效类过滤掉了。后来改成 w-[calc(100%-20px)](去掉空格)就好了,官方文档里其实提了这茬,但我之前没仔细看。
给个我们当时用的 v4 配置示例,都是真实跑在线上环境的:
对了,v4 还有个细节我挺喜欢:之前写 hover:bg-red-500 的时候,如果没在 content 里扫描到对应的类,JIT 不会生成,现在 v4 会在开发环境直接给你抛个警告,告诉你这个类没被找到,省得我每次都要去构建日志里翻半天。我们团队现在新项目全直接上 v4,旧项目也在慢慢迁,毕竟构建速度提升 60% 这种事,对每天要跑几十次开发服务器的我们来说,太香了。
前阵子我们重构了一个日活 120 万的本地生活社区,之前的前端代码是 2019 年写的,用的 Sass + BEM 命名,到重构的时候,光 styles 目录下的 Sass 文件就有 170 多个,加起来 4.7 万行代码。我刚接手的时候,想改个按钮的 hover 颜色,找了半小时才找到对应的 _button.scss 文件,结果改完之后,另一个页面的按钮样式突然崩了——因为之前的开发者写了个全局的 .btn:hover { background: #f0f0f0 },我改的就是这个全局样式。
当时团队里有两种声音:一种说继续用 Sass,把之前的代码整理下,加个 CSS Modules 就行;另一种说直接换 Tailwind。我拉了个对比表,把我们最头疼的几个问题列出来:
.post-card { .title { ... } },结果后来有人写了个全局的 .title 样式,直接把卡片标题的字号改了,排查了两天才发现是嵌套没隔离好。Tailwind 全是原子类,不存在这种问题,每个类的样式都是独立的。p-4 mb-3 text-lg border border-gray-200 rounded-lg hover:border-brand,一行就搞定。我们重构的时候,同样写 20 个组件,用 Sass 花了 3 天,用 Tailwind 只用了 1.5 天。margin-bottom: 16px,编译后就有 10 份。Tailwind 的 JIT 只会生成用到的类,重构后 CSS 体积降到了 110KB,gzip 之后 32KB,页面首屏加载时间从 1.8s 降到了 1.1s,我们统计了下,移动端用户的跳出率直接降了 7 个百分点。当然也不是没遇到坑。我们社区有个帖子详情页,里面的富文本内容是后端返回的 HTML,之前用 Sass 的时候,写了个 .post-content 的样式类,统一设置里面的 p、img、h2 标签的样式。换 Tailwind 之后,我一开始想给每个富文本标签加 Tailwind 类,后来发现后端返回的 HTML 结构不固定,根本没法提前加。后来我用了 Tailwind 的 @apply 指令,在全局样式里这么写:
这样既保留了之前富文本的样式逻辑,又不用把所有样式都改成原子类。不过要注意,@apply 不能乱用,我之前试过把 10 多个原子类堆在一个 @apply 里,结果编译出来的 CSS 居然比直接写原子类还大,后来查了下,JIT 对 @apply 的处理是会把用到的类都打包进去,要是堆太多不常用的类,反而浪费体积。
还有个问题是团队学习成本。我们团队有两个刚毕业的前端,之前没用过 Tailwind,一开始写出来的 HTML 特别长,一个 div 上挂了 20 多个类。我给他们提了个规范:超过 8 个原子类的组件,就封装成 React 组件,比如把卡片的样式封装成 PostCard 组件,内部用 Tailwind 写,外部调用的时候只需要传 title、content 这些 props,不用管样式类。现在他们写代码的速度比之前用 Sass 还快,因为不用再切去写 Sass 文件了,所有样式都在组件里,改起来也方便。
重构完跑了三个月,线上没出现过样式冲突的问题,之前每个月都要花 2-3 天处理样式相关的 bug,现在基本没有了。我们算过一笔账:用 Tailwind 之后,前端开发效率提升了 40%,样式相关的 bug 减少了 85%,这个投入产出比,比继续优化 Sass 划算多了。
上个月我们做了个社区的活动页,要适配手机、平板、桌面三种设备,还要支持深色模式,用户反馈说晚上刷社区眼睛疼,必须得加。我一开始想用媒体查询写两套 CSS,后来算了下,活动页有 12 个模块,写两套的话至少要加 300 多行 Sass,维护起来也麻烦,直接用 Tailwind 的响应式前缀和 dark: 变体搞定了。
先说响应式,Tailwind 默认的断点是 sm: 640px、md: 768px、lg: 1024px、xl: 1280px,我们活动页的 banner 图,手机端要占满宽度,平板端占 80%,桌面端占 60%,我直接这么写:
这里要注意断点的优先级:Tailwind 的响应式前缀是移动端优先的,也就是说 w-full 是所有设备都生效,md:w-4/5 是大于等于 768px 的时候生效,会覆盖之前的 w-full。我之前犯过一个错,把 md:w-4/5 写在 w-full 前面,结果平板端还是显示全宽,后来才知道,CSS 里后面的样式会覆盖前面的,所以小断点的类要写在大断点前面。
然后是深色模式,Tailwind 支持两种方式:一种是 media 策略,跟着系统的深色模式偏好走;另一种是 class 策略,手动切换。我们社区的用户很多是晚上用,所以选了 class 策略,用户可以手动切换,也默认跟着系统走。配置很简单,在 tailwind.config.js 里加个 darkMode: 'class' 就行:
然后写个切换深色模式的组件,我们用了 localStorage 存用户的选择,优先用用户手动选的,没有的话就跟着系统偏好:
这里有个细节:我们之前用 dark:bg-gray-900 当深色背景,结果用户反馈说太黑了,晚上看久了眼睛疼,后来自定义了个 dark-bg: #1a1a2e,稍微带点蓝调,柔和很多。Tailwind 的自定义颜色特别方便,不用改太多代码,直接在 theme.extend.colors 里加就行,所有用了 dark:bg-dark-bg 的地方都会生效。
上线之后我们统计了下,有 62% 的用户开启了深色模式,其中 38% 是手动切换的,剩下的跟着系统走。之前用户反馈晚上刷社区眼睛疼的投诉,从每周 10 多条降到了 0。响应式这块,我们用了 Tailwind 的断点之后,活动页在不同设备上的布局适配率到了 99.7%,之前用媒体查询的时候,偶尔会有平板端布局错乱的问题,现在基本没出现过。
对了,如果你要同时用响应式和深色模式,要注意前缀的顺序:先写响应式前缀,再写深色模式前缀,比如 md:dark:bg-red-500,意思是中等屏幕以上,并且是深色模式的时候,背景是红色。我之前写反了成 dark:md:bg-red-500,结果在手机端深色模式下也生效了,排查了半天才发现顺序错了。
去年我们接手了一个跨境电商平台的重构,客户对品牌色有极其严格的要求,主色值必须是 #1A365D,辅助色是 #F6AD55,且整个系统需要适配 4 种不同国家的子品牌风格。如果直接用默认的 Tailwind 配置,后期维护简直是灾难。我直接放弃了默认主题,基于 Tailwind CSS v4.0 的配置机制,建立了一套可扩展的设计系统。
为什么不直接写内联样式或者覆盖 CSS 变量?因为 Tailwind 的 JIT 编译器在 v4.0 版本对原生 CSS 变量的支持已经非常成熟,通过配置生成的工具类不仅能保证样式一致性,还能在构建时自动剔除未使用的颜色定义,这是手写 CSS 做不到的。
我是这样定义 tailwind.config.js 的,重点在于扩展了 colors 和 spacing,并且为了适配多品牌,我使用了函数式的配置:
在实际组件中,我不再关心具体的色值,而是使用语义化的类名。比如那个订单系统的“发货”按钮:
为什么不这么做会怎样?
如果我不配置 spacing 和 colors,团队成员在写间距时可能会用 p-3 或者 p-[13px] 这种随意的值。在去年的一个后台项目里,因为没有统一间距,导致产品经理要求微调所有卡片的 padding 时,我不得不全局搜索替换了 200 多个文件。统一配置后,只需要修改 tailwind.config.js 里的 spacing 值,或者全局替换 p-4 为 p-5 即可,构建工具会自动处理。
另外,针对深色模式,我采用了 class 策略而不是 media 策略,因为我们需要在网页上提供一个手动切换的按钮,而不是完全跟随系统。在 v4.0 中,这依然是最稳定的方案:
然后在根组件或者 HTML 标签上通过 JS 控制 dark 类的挂载。这样我就可以用 dark:bg-gray-800 这样的类来适配暗黑模式,而不需要写一堆 CSS 变量覆盖。
很多团队在用了 Tailwind 半年后开始抱怨:HTML 里 class 太长了,而且打包出来的 CSS 好像变大了。我在维护一个拥有 300+ 页面的 ERP 系统时也遇到了这个问题。起初我们没注意,导致构建后的 tailwind.css 体积达到了 1.8MB,首屏加载 CSS 耗时 800ms 以上,这在内网环境勉强能用,但在外网环境下直接导致了白屏时间过长。
问题根源在于 content 配置路径不准确。我们之前为了省事,直接配了 ./src/**/*.{js,jsx},导致 JIT 引擎扫描了大量不在构建产物里的测试文件和临时文件。
我做了一次彻底的清理,精确指定了需要扫描的目录,并排除了 node_modules:
优化后,构建体积从 1.8MB 降到了 210KB,首屏 CSS 加载时间降到了 120ms。这个优化是立竿见影的,因为 JIT 编译器不再需要去分析那些无关的文件。
关于 HTML 臃肿,我的观点是:不要试图在 HTML 里写所有逻辑。Tailwind 的设计初衷是“复用工具类”,而不是“复用 HTML 模板”。
我们那个 ERP 系统里有个“状态标签”组件,以前是这么写的(极其冗长):
我直接把这些重复的类封装成了组件,利用 JS 的逻辑复用代替 HTML 的复制粘贴:
为什么不这么做会怎样?
如果不封装,当设计师突然要把所有圆角从 rounded-full 改成 rounded-lg 时,我得改几十个地方。封装后,只改 StatusBadge 组件里的一行代码即可。这比去争论“Tailwind 是不是让 HTML 变丑”要有意义得多。
另外,对于极致的性能要求,我在 v4.0 中尝试了 @apply 的替代方案。虽然官方不推荐滥用 @apply,但在某些全局基础样式上,它依然有用。不过,我更倾向于使用 CSS-in-JS 结合 Tailwind 的方案,比如使用 clsx 库来动态拼接类名,这在处理复杂条件判断时比直接写一堆三元运算符要清晰得多。
去年双十一大促前,我们在压测环境发现了一个诡异的问题:某些动态生成的订单卡片样式在开发环境正常,到了生产构建后就消失了。具体表现为:我们在代码里根据接口返回的 imageHeight 动态拼接了类名 w-[${imageHeight}px],在 next dev 下没问题,但 next build 后,这个类没被生成。
我第一反应是 JIT 引擎没扫描到这个类。因为 JIT 的原理是静态分析你的源代码文件,查找字符串模式。如果你用变量拼接字符串,JIT 是识别不出来的。
为什么这么做不行?
Tailwind 的 JIT 编译器(基于 postcss 插件)在构建时并不会执行你的 JS 代码,它只是做文本扫描。它看到的是 w-[${height}px] 这个字符串,而不是 w-[200px]。所以它在生产环境的 CSS 文件里漏掉了这个类。
我当时的解决方式是利用 style 属性来处理这种极端的动态尺寸,而把 Tailwind 用于静态的、可枚举的样式。
如果一定要用 Tailwind 的动态类(比如为了利用 hover: 等变体),必须保证类名是完整的字符串,且最好是在配置中定义好可能的范围,或者使用 safelist。
那次压测还暴露了另一个问题:构建体积。我们在压测时发现,随着页面访问量增加,CSS 文件并没有因为 JIT 而变小,反而因为 safelist 配置不当变得巨大。
我检查了 tailwind.config.js,发现之前的同事为了“保险”,把一大堆可能用到的类都扔进了 safelist:
这直接导致了构建出的 CSS 包含了大量根本没用到的类。我立刻删除了这个 safelist,转而使用更精准的动态类处理方案。
压测数据对比:
safelist 全开,CSS 体积 450KB,压测 500 QPS 时,服务器内存占用稳定在 1.2GB(主要是 Node.js 构建进程和 Nginx 静态资源缓存)。safelist,CSS 体积 180KB,同样的 500 QPS,内存占用降到了 800MB,且首屏渲染时间(FCP)提升了 15%。这次经历让我明白,JIT 虽然强大,但也不是银弹。在 Tailwind CSS v4.0 中,虽然 JIT 是默认开启的,但你依然需要控制 content 的扫描范围,并且绝对不要滥用 safelist。对于真正的动态样式(如用户自定义主题色、拖拽改变尺寸),老老实实写内联 style 或者用 CSS 变量,比强行塞给 Tailwind 要靠谱得多。
去年接了个电商后台的活儿,要给商家做数据看板。当时团队里两个新来的前端小伙子,一上来就要把整个项目全换成 Tailwind,说写样式快。我拦住了,只让在动态报表卡片那块先试。
结果第一版出来,我就发现不对劲。一个展示销量的卡片,HTML 里塞了十几个类,什么 flex, p-4, bg-white, rounded-lg, shadow-sm 堆在一起,看着就头晕。更要命的是,产品经理突然说要根据后台配置动态生成卡片样式,比如今天显示红色边框,明天显示蓝色阴影。
我本来以为直接在模板里拼字符串就行,结果构建的时候 JIT 死活不认我动态拼接的类名。折腾了半天才反应过来,Tailwind 是静态扫描文件的,你拼出来的 bg-${color}-500 它根本看不到。
最后我是这么解决的:
* 把那些高度动态、需要运行时计算的样式,老老实实写回了 CSS Modules 里,用 CSS 变量去控制颜色。
* 把固定的布局、间距这些不变的结构,继续用 Tailwind 的 grid 和 p-* 来写。
后来我算了一笔账,这个后台项目如果全上 Tailwind,光是 HTML 体积就得涨 30%,而且维护起来全是 text-sm 和 mt-2,找逻辑特别费劲。
我现在觉得,Tailwind 特别适合那种组件化、结构固定的场景,比如公用的 Button、Card 或者官网落地页。但如果你做的是那种像搭积木一样、样式完全由后端数据驱动的复杂业务系统,千万别硬上,否则动态类失效能让你怀疑人生。
学这玩意儿别死记文档,我都是开着编辑器,用到啥查啥。别为了用工具而用工具,代码写得顺手、好改,比啥都强。