前言
这次问题说大不大,说小也不小。
项目里同时用了 Element Plus、Vant、UnoCSS、SCSS mixin、CSS Modules、CSS 变量,还有一批第三方组件库样式。页面本身能正常跑,交互也不卡,但只要打开 Chrome DevTools,在 Elements 面板里选元素、看 Styles、展开 Computed,就会明显卡顿。
就是那种:你只是想改个 padding,浏览器像是开始认真思考人生。
我第一反应是:不会是 UnoCSS 吧?
这个反应很正常。很多人一看到原子化 CSS,就会联想到“类很多、规则很多、DevTools 匹配慢”。再加上项目里还有各种全局样式、主题变量、组件库样式,锅看起来很好甩。
但最后排查下来发现:这次真不是 UnoCSS 的主锅。
这篇文章我主要记录一下这个过程,也就整体两件事:
- DevTools 样式调试卡顿到底怎么一步步定位。
- 在 AI 时代,怎么合理地和 AI 配合,把一个模糊的性能问题拆开、验证、修改、再验证。
我先说结论:AI 很有用,但不能把它当“大仙”。 它适合帮你整理假设、写排查脚本、读代码、批量改代码、总结验证结果。 但最后是不是这个原因,必须靠运行时数据说话。
这次最大的收获不是“删掉了多少 CSS”,而是:不要拿感觉修改性能问题,要拿数据修。AI 也一样。
一、问题到底是什么
问题主要发生在开发调试阶段:
- 页面可以正常打开。
- 业务功能基本可用。
- 页面交互没有明显卡顿。
- 但打开 Chrome DevTools 后,Elements / Styles / Computed 面板明显卡顿。
- 选中元素、切换元素、展开 computed 属性、临时改样式时,体感不顺。
这里要先分清两个概念。
一个是用户态性能:
- 点击是否慢。
- 滚动是否掉帧。
- 动画是否卡。
- 输入是否延迟。
另一个是 DevTools 调试性能:
- DevTools 要读取 CSSOM。
- DevTools 要给当前元素匹配所有可能命中的 CSS 规则。
- DevTools 要展示 cascade 层叠关系。
- DevTools 要算 CSS 变量、继承、覆盖、伪类、媒体查询。
- 如果是 Vite dev 模式,还会有很多
<style>标签通过 HMR 注入。
这次主要是后者。
也就是说,用户不一定卡,但开发者调样式很痛苦。
二、我一开始是怎么“错怪” UnoCSS 的
项目里的样式来源很多:
element-plus/dist/index.css- Vant 组件样式
- UnoCSS
virtual:uno.css @unocss/reset/tailwind-compat.css@xxxx/theme-lib/scss/util.scssAppCommon.scssAppMix.scssScrollbar.scss- CSS Modules
vue-color/style.cssquill/dist/quill.snow.cssanimate.css- 多个
@xxxx/*第三方包 CSS
如果不看数据,光靠经验猜,很容易列出这些嫌疑人:
- UnoCSS 生成了大量工具类。
- SCSS 公共 mixin 被到处注入,重复输出。
- Element Plus + Vant 两套 UI 库 CSS 叠加。
- CSS 变量太多,运行时计算成本高。
- Vue scoped / CSS Modules 生成了很多哈希类名。
- DOM 节点太多。
这些猜想有没有道理?都有一点。
但问题是:有道理不代表它就是主因。
这也是我这次和 AI 沟通时刻意强调的一点:不要上来就让 AI 改代码。
如果你一开始就问:
项目 DevTools 样式卡,是不是 UnoCSS 的问题?帮我去掉 UnoCSS。
AI 很可能会顺着你的话往下走,给你一套迁移方案。看起来很完整,但可能方向就是错的。
更好的问法是:
项目 DevTools 样式调试卡顿,怀疑和 UnoCSS、Element Plus、SCSS 主题变量重复输出有关。先不要改代码,帮我列出可能原因、每个原因的验证方式、优先级和风险。
这个区别很大。
前一种问法是在让 AI 替你背锅。 后一种问法是在让 AI 帮你设计排查路径。
三、AI 在这次排查里真正帮了什么
这次我没有把 AI 当“直接给答案的人”,而是当一个比较耐心的搭档。因为我也怕 一失足成千古恨,再回头已百年身抖音百科(把项目直接搞瘫痪了,😄)。
我主要让它做几类事情。
3.1 先拆假设,不急着改
一开始先让 AI 帮我把嫌疑点拆出来:
- 哪些是构建期问题。
- 哪些是运行时问题。
- 哪些会影响页面性能。
- 哪些只会影响 DevTools 调试体验。
- 哪些低风险,可以先动。
- 哪些高风险,需要单独分支验证。
这样做的好处是,不会一上来就把一个大工程拆烂。
性能问题最怕“我感觉是它,所以我把它删了”。 删完以后问题还在,回归风险倒是先来了。你们有没有遇到过?hahaha
3.2 让 AI 写诊断脚本,而不是让 AI 猜
后面我们需要知道运行时到底加载了多少样式表、多少 CSS rules、最大的是谁。
这种脚本手写不难,但很琐碎。让 AI 写就很合适。
我给它的方向大概是:
写一个浏览器 Console 脚本,统计 document.styleSheets,每个 stylesheet 的 cssRules 数量、ownerNode、textSize、preview,再统计 DOM 节点数量、带 class 的元素数量。
然后拿到了这个脚本:
(() => {
const sheets = [...document.styleSheets];
const getRules = (sheet) => {
try {
return sheet.cssRules ? sheet.cssRules.length : 0;
} catch {
return -1;
}
};
const rows = sheets
.map((sheet, index) => {
const owner = sheet.ownerNode;
const text = owner?.textContent || '';
return {
index,
rules: getRules(sheet),
href: sheet.href || '',
tag: owner?.tagName || '',
id: owner?.id || '',
className: owner?.className || '',
textSize: text.length,
preview: text.slice(0, 120).replace(/\s+/g, ' '),
};
})
.sort((a, b) => b.rules - a.rules);
const allElements = [...document.querySelectorAll('*')];
const summary = {
styleSheets: sheets.length,
totalRules: rows.reduce((sum, item) => sum + Math.max(item.rules, 0), 0),
inaccessibleSheets: rows.filter(item => item.rules < 0).length,
styleTags: document.querySelectorAll('style').length,
linkStyles: document.querySelectorAll('link[rel="stylesheet"]').length,
domNodes: allElements.length,
elementsWithClass: allElements.filter(el => el.className).length,
maxClassCount: Math.max(
0,
...allElements.map(el =>
typeof el.className === 'string' ? el.className.split(/\s+/).filter(Boolean).length : 0,
),
),
};
console.table(rows.slice(0, 30));
console.log('STYLE_DIAG_SUMMARY:', summary);
return { summary, topStyleSheets: rows.slice(0, 30) };
})();
控制台就会有输出,Console 就可以直接复制结果了。
这个过程也挺典型:AI 写脚本,人来跑脚本,人把结果再喂给 AI,AI 再帮你分析。AI 现在是一个比较耐心的搭档。
3.3 把真实输出贴给 AI,让它改判断
最后我们拿到的数据是:
{
"styleSheets": 75,
"totalRules": 4487,
"inaccessibleSheets": 0,
"styleTags": 75,
"linkStyles": 0,
"domNodes": 2055,
"elementsWithClass": 1885,
"maxClassCount": 10
}
Top 样式表里最大的是:
{
"index": 27,
"rules": 2504,
"textSize": 343511,
"preview": "@charset \"UTF-8\";:root{--el-color-white:#ffffff;--el-color-black:#000000;--el-color-primary-rgb..."
}
这个 preview 基本已经把答案写脸上了:Element Plus。
再看 UnoCSS:
- UnoCSS 主样式大概
75 rules - Uno reset 大概
35 rules
所以这个时候就不能继续说“肯定是 UnoCSS”。 数据已经不支持这个结论了。
这也是使用 AI 时很关键的一点:当数据和最初猜想冲突时,要让 AI 跟着数据走,而不是让 AI 继续证明你是对的。
四、第一轮真正动手:清理主题变量重复输出
静态排查时,先发现了一个比较确定的问题:SCSS 公共内容重复输出。
项目在 vite.config.ts 里通过 scss.additionalData 给每个 SCSS 文件注入公共 SCSS:
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/AppCommon.scss" as *;
`,
},
},
},
这个配置不是一定有问题,但前提是:被注入的 SCSS 文件最好只包含变量、函数、mixin,不要包含会输出真实 CSS 的选择器、keyframes、全局规则。
因为每个 .scss、.module.scss、Vue style 都会作为独立入口编译。
如果公共文件里有真实 CSS,那就会被每个入口重复输出。这里可以重点记一下。
4.1 useThemeVar 带来的重复主题块
大量业务 SCSS 中有类似代码:
@use '@xxxx/theme-lib/scss/util.scss' as *;
.foo {
color: useThemeVar('gray1');
}
这会引入主题库里的辅助逻辑,并在 CSS Modules 场景中产生类似这样的块:
._ls-theme__light_xxx {
--ls-color_primary_var: #697dff;
--ls-primary_hover_var: #5464cc;
...
}
如果这种块在很多组件 CSS 里重复出现,CSS 规则数量就会膨胀,DevTools 匹配样式也会更累。
处理方式是把高频 useThemeVar('xxx') 替换成原生 CSS 变量:
color: var(--ls-gray1, #222222);
background: var(--ls-button_gray_bg1, #f3f4f6);
border-color: var(--ls-divider, #e6e6e6);
然后把主题变量统一放到 src/styles/theme-palette.scss,只在全局入口单次引入。
处理后验证:
- PC 入口 CSS 中
_ls-theme__light/_dark = 0 - H5 入口 CSS 中
_ls-theme__light/_dark = 0
这一步属于低风险、高收益。
4.2 第三方依赖里还有残留怎么办
业务源码清掉后,产物中仍有少量 _ls-theme__light 块。
继续追踪发现,不是我们 src 里的代码,而是编译后的 @xxxx/* 依赖包带出来的。
这里没有粗暴删依赖,而是在 vite.config.ts 里加了一个 PostCSS 清理插件,只清理满足严格条件的规则:
- selector 包含
ls-theme__light或ls-theme__dark - declarations 全部是
--ls-*_var
同时对 @xxxx/* 依赖样式里重复的 Uno preflight 变量块做兜底清理。
关键逻辑大概是这样:
function stripRedundantStyleRules() {
return {
postcssPlugin: 'strip-redundant-style-rules',
Once(root: any, { result }: any) {
const from = String(result?.opts?.from || '')
const isDependencyStyle = from.includes('/node_modules/') && from.includes('/@xxxx+')
root.walkRules((rule: any) => {
if (isThemeLibVarBlock(rule)) {
rule.remove()
return
}
if (isDependencyStyle && isUnoPreflightVarBlock(rule)) {
rule.remove()
}
})
},
}
}
注意这里的重点不是“写了个插件很厉害”,而是它只删可证明重复的东西。 性能优化里,最怕那种“看着没用就删”的操作。
五、第二轮:setting-form.scss 重复输出
后面继续看最新 CSS chunk,发现 TextInput 懒加载 CSS 中有大量表单相关规则重复命中。
原因是 src/styles/setting-form.scss 里直接定义了类:
.setting-form-basic {
:global {
.form-item-vertical {
...
}
}
}
但很多 CSS Module 只是这样写:
@use '@/styles/setting-form.scss';
即使没有真正使用 .setting-form-basic,Sass 也会把 setting-form.scss 里的真实 CSS 输出进当前模块。
处理方式是把它改成纯 mixin:
@mixin setting-form-basic {
:global {
.form-item-vertical {
...
}
}
}
调用方改成:
@use '@/styles/setting-form.scss' as setting-form;
.name-wrapper {
@include setting-form.setting-form-basic;
}
无用 @use 删除。
效果:
TextInput懒加载 CSS 中表单相关命中从约120降到约29。- 剩余基本是组件自身
.el-form-item__*样式,不是公共 SCSS 重复输出。
这个点也很适合让 AI 辅助处理。 因为它不是特别难,但涉及文件很多,人工一个个找容易漏。AI 擅长做这种“按规则批量替换 + 构建验证”的工作。
但这里也要注意:让 AI 改之前,要先把规则讲清楚。
比如:
只删除没有实际使用 mixin 的
@use '@/styles/setting-form.scss'。如果文件里确实需要.setting-form-basic,改成显式@include。不要改其他样式。
规则越明确,AI 越不容易乱发挥。
六、第三轮:把不该全局加载的第三方 CSS 挪出去
6.1 Quill 样式
原来 PC 入口直接引入:
import 'quill/dist/quill.snow.css'
但 Quill 只服务表达式编辑器,不应该污染设计器主入口 CSS。
处理方式是从 src/entries/create-pc-app.ts 移除,改到表达式编辑器组件里:
// src/components/express-editor/index.vue
import 'quill/dist/quill.snow.css'
结果:
- PC 入口 CSS 中
.ql-规则为0。 - Quill 样式跟随表达式编辑器 chunk 加载。
6.2 animate.css
项目实际使用的动画类很少:
animate__animated
animate__faster
animate__slideInRight
animate__slideOutRight
animate__shakeX
但原来引入的是完整 animate.css:
import 'animate.css'
这就有点像只想吃一口饭,结果点了一桌席。
处理方式是新增 src/styles/animate-lite.scss,只保留实际用到的动画类和 keyframes:
.animate__animated {
animation-duration: 1s;
animation-fill-mode: both;
}
.animate__faster {
animation-duration: 0.5s;
}
.animate__slideInRight {
animation-name: animateSlideInRight;
}
.animate__slideOutRight {
animation-name: animateSlideOutRight;
}
.animate__shakeX {
animation-name: animateShakeX;
}
然后在 global.scss 单次引入:
@use '@/styles/animate-lite.scss' as *;
同时清理 vite.config.ts 中 H5 manualChunks 里已经无用的配置:
'vendor-quill': ['quill'],
'vendor-animate': ['animate.css'],
否则即使源码不 import,打包配置也可能继续把它拉进产物。
七、为什么说 UnoCSS 不是主因
中间最关键的转折,就是我们真的去量了 UnoCSS 的占比。
项目确实使用了:
presetWind3({
prefix: 'ls-',
})
UnoCSS 在 Vite 中会扫描源码按需生成工具类,不会像 Tailwind 那样预加载整套完整规则库。
运行时数据里:
- UnoCSS 主样式约
75 rules - Uno reset 约
35 rules - Element Plus 全量样式约
2504 rules
这个量级差距很明显。
所以“去掉 UnoCSS 能解决主要卡顿”的判断,在这个项目里不成立。
这一步对我挺有提醒意义: 我们平时很容易根据技术标签判断问题,比如“用了 Tailwind 所以 CSS 多”“用了 scoped 所以慢”“用了 CSS 变量所以卡”。但真实项目里,最后还是要看产物和运行时。
AI 也一样。
你给它一个强引导,它可能会顺着你说。 你给它真实数据,它就能帮你重新排序嫌疑人。我们也是讲证据的。
八、最终效果
构建后的入口 CSS 有明显下降。
PC 入口 CSS:
| 阶段 | bytes | rules |
|---|---|---|
| 清理前大约 | 556KB | 5332 |
| 当前 | 461KB | 4251 |
当前 PC:
{
"file": "index-B-viWbtn.css",
"bytes": 461538,
"rules": 4251,
"lsTheme": 0,
"ql": 0,
"animate": 5,
"unoDecl": 154
}
当前 H5:
{
"file": "index-XRJnZ5i7.css",
"bytes": 354690,
"rules": 3342,
"lsTheme": 0,
"ql": 0,
"animate": 5,
"unoDecl": 102
}
已确认的效果:
- 重复主题块清零。
setting-form.scss不再到处重复输出。- Quill 样式不再进入 PC 主入口。
- animate.css 全量包不再进入入口。
- UnoCSS 占比很低,不再作为主要优化对象。
- DevTools 卡顿体感改善,但仍有轻微卡顿。
九、为什么还有一点卡顿
处理到目前,效果已经好很多了,但是仍有轻微卡顿。
因为当前最大头还在:
element-plus/dist/index.css
运行时它大概是:
2504 rules
343511 textSize
这一份样式表就超过总规则量一半。
如果继续优化,最大收益点就是 Element Plus 按需化。
但它风险也最大。
原因是:
- 项目用了
app.use(ElementPlus)。 - 很多组件依赖全局注册。
- 消息、弹窗、选择器、表格、表单、树组件都可能间接依赖 Element Plus 样式。
- 直接删除全量 CSS,可能导致一些浮层或边缘组件样式缺失。
所以它不适合在当前分支直接硬改。 正确做法是单独开实验分支,明确验证清单和回滚标准。
下一步可以实验:
// 移除
import 'element-plus/dist/index.css'
// 改成 resolver 按需注入
ElementPlusResolver({ importStyle: 'css' })
成功标准:
- 构建通过。
- 页面主要组件样式正常。
- 运行时不再存在
2500 rules级别的 Element Plus 全量样式。 totalRules明显下降。- DevTools Computed 面板体感继续改善。
失败标准:
- 大量 Element Plus 样式缺失。
- 浮层组件异常。
- 需要手工维护大量样式 import。
- 规则数下降不明显。
如果 Element Plus 按需化风险太高,再继续拆这些局部第三方 CSS:
vue-color/style.css@xxxx/attr-selector/dist/index.css@xxxx/js-high-code/dist/index.css@xxxx/data-comparision/dist/index.css@xxxx/user-contact/dist/index.css
但每次只动一个,改完就构建、跑页面、重新统计。
十、这次我是怎么更好地用 AI 的
这次排查下来,我觉得 AI 真是个好伙伴。
真正好用的方式,不是问:
帮我优化一下性能。
这个问题太大了,AI 容易给一堆正确但没用的话。
更好的方式是分阶段问。
10.1 第一阶段:让 AI 帮你列假设
可以这样问:
DevTools Elements / Computed 面板调试样式很卡。项目用了 Element Plus、Vant、UnoCSS、CSS Modules、SCSS additionalData、CSS 变量。先不要改代码,帮我列出可能原因、验证方式和优先级。
这一步的目标不是拿答案,而是拿排查地图。
10.2 第二阶段:让 AI 帮你写可执行的诊断脚本
可以这样问:
写一个浏览器 Console 脚本,统计当前页面所有 stylesheet 的 rules 数、文本大小、ownerNode、preview,并统计 DOM 节点数量。结果要方便复制给你继续分析。
这个阶段 AI 很擅长。
因为它不需要猜业务,只是在帮你快速生成工具。
10.3 第三阶段:把真实数据贴回去
不要只说“还是卡”。
要把数据贴回去:
这是运行结果:styleSheets 75,totalRules 4487,最大 stylesheet rules 2504,preview 是
:root{--el-color...},UnoCSS 只有 75 rules。重新判断主因和下一步。
这样 AI 才会从“可能是 UnoCSS”修正到“更可能是 Element Plus 全量 CSS”。
10.4 第四阶段:让 AI 先做低风险改动
比如:
先不要动 Element Plus。先找出项目里重复输出的主题变量块、公共 SCSS 真实 CSS 输出、入口里不该全局加载的第三方 CSS。每次只做一类改动,改完跑构建。
这样比较稳。
别一上来就让 AI 做大迁移。 大迁移看起来爽,回归起来也很爽。
10.5 第五阶段:让 AI 帮你写验证标准和回滚方案
这一点很多人会忽略。
我们平时问 AI,常常只问“怎么改”。 其实更应该问:
怎么证明这个改动有效?怎么证明它没引入回归?如果失败,怎么回滚?
这次 Element Plus 按需化之所以没有直接硬上,就是因为验证清单和失败标准很明确。
这也是 AI 特别适合做的事: 帮你把脑子里模糊的“应该没问题吧”,变成具体的 checklist。
十一、这次总结出来的几条经验
11.1 性能问题不要靠猜
这次最开始很容易怀疑 UnoCSS,但数据证明它不是主因。
判断样式性能问题时,至少要拿到这些数据:
- CSS rule 数量
- stylesheet 来源
- DOM 节点数量
- 最大 style tag
- 构建产物规则数
- dev 模式和生产模式的差异
11.2 公共 SCSS 要分清“定义”和“输出”
适合被多处 @use 的文件:
- 变量
- 函数
- mixin
- placeholder
不适合被多处 @use 的文件:
- 普通 class
- 全局选择器
- keyframes
- reset
如果公共文件会输出真实 CSS,就应该只在入口单次引入。
11.3 第三方 CSS 不要无脑放入口
全局入口只适合放真正全局必需的样式。
像 Quill、颜色选择器、动画库这类只在少数功能中使用的 CSS,尽量跟随组件 chunk 加载。
11.4 Dev 模式和生产模式要分开看
Dev 模式:
- 多个
<style>标签 - HMR 注入
- source map / module style 更碎
生产模式:
- CSS 更可能合并成文件
- 规则数量可能相近,但 stylesheet 组织方式不同
DevTools 卡顿在 dev 模式下更明显,不一定代表用户生产环境同等卡顿。
11.5 AI 能加速,但不能代替验证
AI 可以帮你:
- 整理嫌疑点。
- 写诊断脚本。
- 搜索重复模式。
- 批量改低风险代码。
- 生成验证 checklist。
- 写复盘文档。
但 AI 不能替你:
- 判断页面真实体感。
- 替代构建结果。
- 替代运行时数据。
- 替代视觉回归检查。
- 替代你对业务风险的判断。
说白了,AI 是放大器。 你给它猜测,它会放大猜测。 你给它数据,它会放大分析能力。
十二、最终结论
这次优化不是简单地“干掉某个库”,而是一次逐层收敛:
- 先列嫌疑点。
- 再写脚本拿运行时数据。
- 然后清掉确定重复的主题变量块。
- 再清掉公共 SCSS 的重复输出。
- 再把不该出现在入口的第三方 CSS 挪到实际使用组件。
- 最后用运行时数据确认剩余瓶颈。
目前剩余最大点是 Element Plus 全量 CSS。 它值得继续实验,但必须单独开分支、单独验证、明确回滚标准。
最后再说回标题:
UnoCSS,这次确实是我错怪你了。
CSS 性能优化最怕“按感觉删库”。 AI 时代也一样,最怕“让 AI 按你的感觉证明你是对的”。
更可靠的方式是:
把 AI 当搭档,不当裁判;把猜想变成数据,把数据变成决策。