less.render 不支持流式编译,因其仅接受字符串输入、返回字符串输出,无流接口,高并发下易致内存堆积、事件循环阻塞及OOM。

Node.js 默认的 less.render 是回调式异步编译,不支持流式处理,高并发下容易堆积内存、阻塞事件循环。真要流式编译,得绕开官方 API,用 less.parse + tree.toCSS 手动构造可流式管道,或改用 lessc 子进程 + stdin 输入。
为什么不能直接用 less.render 做流式编译
less.render 接收字符串输入、返回字符串输出,内部会完整解析 AST 并一次性生成 CSS。它没有 stream.Readable 或 stream.Transform 接口,无法分块读取、也无法边解析边吐出 CSS 片段。在 QPS > 50 的场景下,大量并发调用会导致 V8 堆内存快速上涨,GC 频繁,甚至 OOM。
常见错误现象:RangeError: Maximum call stack size exceeded(嵌套过深)、FATAL ERROR: Ineffective mark-compacts(内存不足)、响应延迟从几 ms 涨到数秒。
- 它不支持传入
Readable流作为源,只接受string或Buffer - 所有选项(如
paths、plugins)必须提前确定,无法运行时动态注入 - 错误堆栈不包含原始 LESS 行号(除非开启
sourceMap,但会加重开销)
用 child_process.spawn 启动 lessc 实现准流式编译
这是最稳妥、可横向扩展的做法:把编译逻辑隔离到独立进程,主进程只负责转发数据、回收 stdout/stderr。每个请求独占一个 lessc 实例,避免状态污染,也规避了 Node.js 单线程瓶颈。
立即学习“前端免费学习笔记(深入)”;
实操要点:
- 不要用
exec或execSync—— 它们会缓冲全部 stdout,失去“流”意义;必须用spawn('lessc', ['--no-js', '--strict-math=on'], { stdio: ['pipe', 'pipe', 'pipe'] }) - LESS 输入必须通过
proc.stdin.write()发送,结尾加proc.stdin.end(),否则lessc不会触发编译 - 务必监听
proc.stderr—— 编译错误(如语法错、@import 路径不存在)全走这里,proc.stdout只有成功时才吐 CSS - 设置超时:
proc.kill('SIGTERM')+setTimeout,防止卡死进程(LESS 里死循环@import或无限递归 mixin 会 hang 住)
示例关键片段:
const { spawn } = require('child_process');
const proc = spawn('lessc', ['--no-js', '--strict-math=on'], {
stdio: ['pipe', 'pipe', 'pipe'],
});
proc.stdin.write(lessSource);
proc.stdin.end();
proc.stdout.on('data', chunk => res.write(chunk));
proc.stderr.on('data', err => console.error(err.toString()));
proc.on('close', code => code !== 0 && res.statusCode = 500);
用 less.parse + tree.toCSS 手动构建流式 pipeline
如果你坚持纯 JS 方案(比如部署环境禁止 spawn 子进程),就得手动拆解编译流程:先 less.parse 得到 AST,再用 tree.toCSS 输出 CSS。虽然仍是内存中操作,但能控制中间态,配合 Transform 流做轻量级流式封装。
注意点:
-
less.parse返回的是 Promise,不是流,但你可以把它包装成Readable.from([lessSource])后接Transform,在_transform里调用less.parse - 必须显式传入
paths和filename,否则@import会失败 ——lessc默认用当前工作目录,而parse默认无上下文 -
tree.toCSS支持sourceMap和compress,但不支持math选项(如--strict-math),需靠less.parse的math参数控制 - 每次调用都会新建
less.Parser实例,不能复用 —— 复用会导致缓存污染、变量作用域错乱
高并发下真正容易被忽略的点
不是编译快慢的问题,而是资源边界失控。
-
lessc进程没设ulimit -n限制,1000 并发可能瞬间打开 2000+ 文件描述符,触发系统级拒绝服务 - 没做进程池(如
worker_threads或cluster.fork),所有请求挤在同一个lessc实例上,反而比单线程更慢 - LESS 文件里用了
@plugin,而插件是同步加载的 —— 每次都 require(),模块缓存失效,CPU 白耗 - 忘记关闭
proc.stdin或没监听proc.on('error'),子进程异常退出后残留僵尸进程,撑爆max user processes
实际压测时,建议用 lessc --version 校验子进程是否真的启动成功,别只信 spawn 返回值。


















