less编译器先构建依赖图并递归展开@import,未启用(once)时相同路径多次导入会重复解析ast;路径字符串差异即视为不同文件,需统一paths配置和收敛入口链。

Less 编译器如何解析 @import 链
Less 不是按文件顺序线性编译,而是先构建完整的依赖图——每次遇到 @import,就递归展开其内容,并检查是否已被导入过。若未启用 @import (once),相同路径被多次引用(比如五个组件都 @import "mixins.less"),就会重复解析、词法分析、AST 构建,而这些操作不共享中间结果。
更麻烦的是,路径差异会被视为不同文件:@import "./mixins" 和 @import "../shared/mixins" 即使最终指向同一物理文件,Less 也当作两个独立模块处理,导致 AST 重复生成。
- 避免相对路径泛滥:统一配置
paths: ["src/styles"],所有导入写成@import "mixins/button" - 禁用通配符导入:
@import "components/*.less"在多数 Less 版本中不被支持,且破坏可预测性 - 入口文件只保留一个
@import (once)链,其他地方禁止直接 import 公共文件
嵌套与运算如何拖慢词法分析阶段
Less 的嵌套语法(如 .card { .header { color: @text-color; } })在解析时需维护作用域栈;每层嵌套都会增加 AST 节点深度和符号表查找开销。当嵌套超过 3 层,不仅输出 CSS 选择器过长,编译器本身在作用域解析上就要多做数倍的字符串拼接和变量回溯。
同理,calc() 或自定义函数中的运行时计算(如 lighten(@primary-color, 10%))不会被提前折叠,而是在每次调用处重新执行颜色模型转换——这不是缓存友好的纯函数调用,而是带状态的解析过程。
- 把视觉逻辑拆到 mixin 参数里,而非在嵌套中动态算值:
.btn(@bg) { background: @bg; }比.btn { background: lighten(@primary-color, 10%); }更快 - 用
&控制嵌套输出结构,避免无意义的层级膨胀:.card { &__body { padding: 1rem; } }编译为.card__body,不增加解析深度 - 禁用开发期
math: always(默认行为),改用strictMath: true,强制运算必须显式括号,减少歧义解析耗时
为什么磁盘 I/O 成为隐性瓶颈
Less 默认不缓存已解析的 AST,每次构建都从头读取、解析、验证每个 @import 文件。当项目有 200+ 个 .less 文件,平均单文件 50 行,总行数超万行时,仅文件系统调用和字符流解析就占编译总时长的 40% 以上——这和代码逻辑无关,纯粹是 I/O 和内存分配开销。
Webpack 的 less-loader 默认不开启持久化缓存,即使内容未变,也会重复触发 less.render() 调用;而 Vite 的 vite-plugin-less 同样默认跳过 AST 复用。
- 在
less-loader中启用cache: true并指定cacheDirectory,让解析结果落盘复用 - Vite 用户应搭配
vite-plugin-style-import或手动配置css.preprocessOptions开启缓存 - 避免在
@import中使用动态路径或变量:@import "@{theme}/base.less"会彻底禁用该处缓存
真正卡住的不是“编译”,是依赖图的拓扑排序
Less 编译慢的本质,不是语法转换慢,而是解决“谁该先编译”这个问题的成本越来越高。它要把所有 @import、@plugin、变量赋值、mixin 定义按依赖关系拓扑排序——这个过程的时间复杂度接近 O(n²),其中 n 是模块数量。当组件数从 50 增至 500,理论耗时可能从 300ms 涨到 3s 以上。
这时候加 CPU 核心数没用,因为排序本身是强顺序依赖;压缩选项也无效,因为压缩发生在最后阶段,前面的解析和排序早已卡死。
最容易被忽略的一点:你删掉一半样式文件,编译速度未必提升一半——如果删的是叶子节点,影响有限;但若删的是被 80 个文件 import 的 variables.less,整个依赖图会大幅简化,速度可能翻倍。优化得先看依赖热力图,而不是盲目删代码。











