我们团队去年接手了一个运行了四年的电商后台管理系统,代码量大概在 12 万行左右。打开样式目录的时候,我整个人都愣住了:所有的 SCSS 文件都在 styles 目录下平铺,没有任何层级,而且几乎每一个文件的第一行都是 @import "variables"; 和 @import "mixins";。
当时项目还在用 LibSass,编译一次全量样式平均需要 800ms。最要命的是,有一次我改了一个全局变量 $primary-color,结果因为 @import 的加载顺序问题,导致某个深层组件里的按钮颜色没变,而另一个弹窗的颜色却变了,排查了整整两个小时。
后来我们决定直接升级到 Dart Sass 1.77.0(2024年5月发布的最新稳定版),并彻底干掉 @import,全面迁移到 @use 和 @forward。
为什么一定要换?@import 最大的问题就是它没有命名空间。它就像是你把所有的工具都倒在一个大箱子里,变量、函数、混合宏全部全局暴露。如果你在 a.scss 里定义了一个 $color,在 b.scss 里也定义了一个 $color,@import 之后谁覆盖谁全看运气。而 @use 则像是一个个贴好标签的工具盒,你用哪个拿哪个,互不干扰。
迁移的过程其实比想象中要有趣。我们第一步是把 variables.scss 改造成了模块。
这是改造前的 variables.scss:
改造后,我们在 styles/_index.scss 里统一管理出口:
然后在具体的组件里,我们不再全局引入,而是按需加载:
这里有个细节。刚开始我们想偷懒,直接用 @use '...' as * 来屏蔽命名空间,后来发现这又回到了 @import 的老路上,失去了模块化的意义。于是我们强制规定,除了 _index.scss 这种聚合文件,其他文件必须带命名空间调用。
还有一个实际遇到的问题:旧代码里大量使用了 @import 来引入第三方库(比如之前的 normalize.css)。我在迁移时发现,Dart Sass 1.77.0 虽然还兼容 @import,但在编译日志里会疯狂报警告。为了彻底解决,我写了一个小脚本,把所有的 @import 'xxx' 替换成了 @use 'xxx',但对于那些不需要命名空间的纯 CSS 文件,我们保留了 @import,因为 @use 对于纯 CSS 文件来说有点杀鸡用牛刀。
迁移完成后,最直观的变化是编译速度。全量编译从 800ms 降到了 120ms 左右。因为 @use 机制下,同一个模块只会被加载一次,而 @import 每次都会重新解析。另外,样式体积也小了大概 5%,因为避免了重复加载带来的冗余代码。
如果你现在还在用 @import,我建议趁着项目还没大到无法维护,赶紧动手。Dart Sass 官方已经明确表态,@import 终将被移除,现在迁移是为了以后少掉头发。
我之前负责过一个社区产品的重构,日活大概在 150 万左右。产品经理提了一个需求:不仅要支持亮色和暗色模式,还要允许用户自定义主题色。当时我们面临的选择是:用 CSS 变量还是 Sass 变量?
很多人会直接说用 CSS 变量,因为可以运行时动态切换。但我当时做了一个混合架构:编译时用 Sass 变量处理基础逻辑,运行时用 CSS 变量处理动态切换。
为什么不全用 CSS 变量?因为我们的组件库非常复杂,很多地方需要根据主题色计算出不同深浅的阴影、悬停色。如果全用 CSS 变量,在 Sass 编译阶段这些变量是死的,没法用 darken() 或 lighten() 函数处理。
我是这么设计的。在 styles/theme/_variables.scss 里,我定义了 Sass 变量作为“源数据”:
然后,我写了一个 @mixin 来生成 CSS 变量。这个 @mixin 接收一个主题 Map,把它转换成 CSS 变量:
在入口文件里,我根据一个 Sass 变量 $current-theme 来决定生成哪个主题的 CSS 变量。但这还没完,因为用户可能随时切换暗色模式。
所以我在 styles/theme/_index.scss 里做了这样的事:
在组件里,我不再直接写颜色值,而是用 var() 去读取这些 CSS 变量:
注意上面那个 hover 的处理。为了让 rgba 能工作,我在生成 CSS 变量的时候,还得把颜色拆成 RGB 格式:
这个架构上线后,表现非常稳。用户切换主题时,只是改变了根节点上的 CSS 变量,不需要重新加载样式表。而 Sass 在编译阶段帮我们处理了所有的逻辑运算和变量生成,兼顾了开发效率和运行性能。我们当时测过,切换主题的响应时间基本在 30ms 以内,用户完全感知不到卡顿。
很多刚接触 Sass 的朋友都会纠结:@mixin 和 @extend 到底该用哪个?我在一次性能优化专项里,专门对这两个特性做了一次压测。
当时我们的项目里有一个 clearfix 样式,大概有 200 多个组件都在用。之前有个同事为了图省事,直接写了一个 @mixin clearfix,然后在每个组件里 @include。
我写了一个脚本,模拟了 200 个组件分别使用 @mixin 和 @extend 的情况,然后对比编译后的 CSS 体积和编译耗时。
压测环境:Dart Sass 1.77.0,MacBook Pro M1,单文件编译。
方案一:Mixin(传参复用)
方案二:Extend(继承复用)
压测结果对比:
| 特性 | 编译后 CSS 体积 | 编译耗时 | 生成代码特征 |
| :--- | :--- | :--- | :--- |
| @mixin | 约 12.5 KB | 45ms | 每个选择器都重复包含完整的 ::after 代码块 |
| @extend | 约 2.8 KB | 120ms | 所有选择器合并成一个逗号分隔的列表,共享一个 ::after 代码块 |
数据很直观。Mixin 的体积是 Extend 的 4 倍多。因为 Mixin 是“复制粘贴”逻辑,你 @include 一次,它就复制一份代码到那个位置。而 Extend 是“合并同类项”,它把用了 @extend 的选择器全部拎出来,放在一起写一次样式。
但是,编译耗时上 Extend 反而更慢。这是因为 Extend 需要分析整个文档的选择器关系,进行合并计算,这是一个相对耗时的过程。不过对于 200 个组件来说,多出来的 75ms 完全可以忽略不计,毕竟体积优化带来的网络传输收益更大。
那是不是应该无脑用 Extend?
并不是。我遇到过一个坑。有一次我想用 Extend 来继承一个 .btn 的样式,结果编译出来的 CSS 里,.btn 这个类名也被输出了。但在我的 HTML 里,我其实只想用 .btn-primary,并不想让 .btn 这个类出现在 DOM 里(虽然没影响,但看着不舒服)。
后来我学乖了,用占位符选择器 %。就像上面的压测代码里写的,用 %clearfix 而不是 .clearfix。这样编译后的 CSS 里就不会出现 .clearfix 这个多余的类。
我的选型策略是这样的:
@mixin。比如一个圆角 Mixin:@mixin rounded($radius: 4px) { border-radius: $radius; }。Extend 没法传参,这是硬伤。@extend。比如 clearfix、ellipsis(文字溢出省略)这种不需要参数的工具类。@extend。Extend 在跨媒体查询使用时会有问题。比如你在 @media (max-width: 768px) 里 @extend 了一个在全局定义的 %ellipsis,Dart Sass 会报错,因为它没法把两个不同作用域的选择器合并在一起。这种情况下,我宁愿用 Mixin 或者通过增加 HTML 类名来解决。所以,回到那个 200 多个组件的 clearfix 问题,我最后的方案是把 Mixin 改成了 Extend,体积直接砍掉了 10KB 左右。对于首屏加载来说,这 10KB 的优化还是很有意义的。
去年我在重构一个电商后台管理系统时,遇到了一个非常头疼的问题。这个项目迭代了三年多,样式文件膨胀到了 1200 多个,SCSS 代码量超过 8 万行。随着业务逻辑越来越复杂,我每次修改一个按钮的样式,按下保存键后,浏览器就像死机了一样,HMR(热更新)的转圈动画至少要转 3 到 5 秒才能看到变化。有时候甚至直接触发了 Webpack 的重新全量编译,等待时间长达 15 秒。
作为开发者,这种等待是极其消磨耐心的。当时我查了一下环境,项目使用的是 Dart Sass 1.69.5,构建工具是 Vue CLI 5(基于 Webpack 5)。我决定彻底排查一下这个问题。
我做的第一件事是用 time 命令和 Node.js 的 performance API 测量了一次完整的编译耗时。结果发现,编译这 8 万行 SCSS 竟然需要 850ms。而 HMR 之所以慢,是因为每次修改都会触发整个 main.scss 的依赖树重新计算。
排查过程与根因分析
我打开项目的 main.scss,发现了典型的“意大利面条式”引用。我们使用了旧的 @import 方式,导致每一个文件都会重复引入 variables.scss 和 mixins.scss。
在 Dart Sass 1.77.0(2024年5月发布的最新版本)中,虽然编译速度已经比早期的 Ruby Sass 快了很多,但 @import 是一个没有命名空间、没有缓存机制(在旧逻辑下)的指令。它会导致变量和 Mixin 在内存中被反复加载和解析。
解决方案:迁移至 @use 与升级 Dart Sass
我制定了一个迁移方案。首先,我将 Dart Sass 升级到了 1.77.0,这是目前的官方主流实现,替代了已经停止维护的 LibSass。然后,我写了一个 Node.js 脚本,批量将 @import 替换为 @use。
这是改造后的代码结构:
为什么这么做?
@use 规则最大的不同在于,同一个模块在同一个编译上下文中只会被加载一次。无论你在 100 个文件里 @use 'variables',Sass 在编译时只会处理一次变量定义。这直接把我的编译时间从 850ms 降到了 120ms。
关于 HMR 的进一步优化
即便编译快了,Webpack 的 HMR 依然可能因为依赖图过大而卡顿。我发现项目中很多 SCSS 文件里写了大量的 @each 循环来生成辅助类(Utility Classes)。
这种逻辑在开发模式下也会全量执行。我采取的策略是:开发环境动态生成,生产环境按需生成。
我修改了 vue.config.js 的配置,利用 sass-loader 的 additionalData 注入环境变量:
然后在 SCSS 中做判断:
经过这一轮优化,我们的 HMR 响应时间稳定在 200ms 以内,开发体验有了质的飞跃。这让我意识到,在大型项目中,Sass 不仅是写样式,更是在管理编译性能。
---
最近社区里经常讨论一个问题:用了 Tailwind CSS 还需要 Sass 吗?甚至有激进的观点认为 Sass 已经过时了。作为经历过从写 CSS 到 Less,再到 Sass,现在又接触 Tailwind 的开发者,我在最近的一个 SaaS 后台项目中尝试了一种混合架构:用 Sass 作为逻辑层(Logic Layer),用 Tailwind 作为原子类层(Atomic Layer)。
这种架构并不是简单的“混用”,而是有明确的分层逻辑。
为什么需要混合?
Tailwind 非常适合快速搭建 UI 骨架,比如间距、排版、颜色。但是,我在做那个 SaaS 项目时遇到了一个场景:我们需要实现一个复杂的“数据看板”,里面有 20 多种不同状态的卡片,每种卡片都有独特的渐变背景、阴影和动画。
如果纯用 Tailwind,我的 HTML 会变成这样:
这种代码不仅难以维护,而且一旦设计稿微调(比如把 purple-600 改成 indigo-600),我得全局替换,或者去改 tailwind.config.js。
我的解决方案:Sass 负责“设计系统”,Tailwind 负责“快速排版”
我决定保留 Sass 的强项——逻辑抽象。我利用 Sass 的 Mixin 和 Functions 来定义那些复杂的、需要计算的状态样式。
具体做法是,我在 styles/ 目录下建立了一个 components/ 文件夹,专门用 SCSS 写“高阶组件样式”。
如何在 HTML 中协同?
在 HTML 层面,我不再写一大堆 Tailwind 类来定义样式,而是把 Tailwind 用作“布局工具”,把 Sass 定义的类用作“皮肤”。
这种架构的优势在哪里?
@function 去计算颜色亮度,或者根据状态生成不同的阴影。这比在 tailwind.config.js 里写一堆 extend 要灵活得多。hover:bg-[${color}]),它有时会失效。而 Sass 在编译时就把这些逻辑处理好了,生成的 CSS 非常干净。shadow-lg 或者 shadow-red-500 并手动调整。但在我的混合架构里,我只需要修改 variables.scss 里的一个映射值 $status-config,然后重新编译即可。这种“Sass 做逻辑,Tailwind 做布局”的模式,特别适合那种设计规范复杂、状态多、且需要长期维护的企业级项目。它不是为了炫技,而是为了在开发效率和代码可维护性之间找一个平衡点。如果你正在纠结选 Tailwind 还是 Sass,不妨试试这种“双剑合璧”的方案。
去年我接手了一个跑了四年的老项目,是一个日活过百万的社区。接手时我头都大了,Sass 文件嵌套深得像迷宫,一个按钮的样式要穿透七八层才能找到源头。最要命的是,他们还在用 @import,每次改一个变量,整个项目几百个文件全部重新编译,HMR 热更新能卡十几秒,改个颜色我都能去接杯水回来。
我当时第一反应就是全量迁移到 @use 和 @forward。但真动起手来才发现,老代码里到处是全局变量和函数的隐式依赖。我花了整整一个周末,一边跑着编译脚本看依赖树,一边手动拆解模块。最后我做了一个决定:不追求一步到位。我把核心的主题变量(颜色、字体)先抽离成独立的 theme 模块,业务组件暂时保留旧的引用方式,通过一层适配文件慢慢过渡。
折腾完之后,编译速度从原来的 8 秒降到了 2 秒以内,开发体验直接起飞。
关于 Sass,我有几个不吐不快的实话:
* 别迷信 @extend:很多人喜欢用 %placeholder 来复用样式,觉得生成的 CSS 体积小。但在大型项目里,这玩意儿极其容易导致选择器无限制膨胀。我在压测里发现,超过三层继承,生成的 CSS 体积反而比 @mixin 大。我现在基本只用 @mixin,除非是那种极其简单的基类。
* 避免过度嵌套:Sass 给的嵌套能力太爽了,但千万别超过三层。我见过有人写 .header .nav .list .item .link,这种代码维护起来就是灾难。
* 什么时候别用:如果你只是做个简单的官网或者活动页,真没必要上 Sass。直接写原生 CSS 配合 CSS Variables,反而更轻量,构建配置也省心。
学 Sass 别光看文档里的语法糖,多去看看别人怎么组织文件结构的。很多时候,决定你代码质量的不是你会不会写 @function,而是你能不能把 _variables.scss 设计得让人一眼看懂。遇到编译慢了,先别怪工具,看看是不是自己的嵌套层级或者模块引用乱了套。