案发
你可能很难想象,压垮一台 32G 内存笔记本的,不是深度学习模型,也不是 3D 渲染,而是一个简简单单的 next dev。
事情是这样的。
那天晚上,我像往常一样打开项目,敲下 pnpm next dev,打算继续写那个拖了一周的页面。一切看起来都很正常——终端启动、编译、热更新……直到我开始感觉到电脑的风扇在狂转。
我扫了一眼任务管理器,node.exe,内存占用:1.6GB。
五分钟后,2.4GB。又过了几分钟,直逼 4GB。
我当时的第一反应是:我代码里是不是写了什么死循环?还是哪个 useEffect 在无限请求?我停下所有操作,把浏览器切到后台,看着内存条的数字在静默中一点一点往上爬——没有触发任何代码执行,仅仅是 IDE 开着、终端挂着。
也就是说,进程在“闲着”的时候,也在疯狂吃内存。
这不是我写的 Bug。这是工具在自杀。
破案:不是代码,是路径
我把排查范围缩窄到 Turbopack——Next.js 16 默认搭载的那个号称“快 700 倍”的编译引擎。
由于是 Rust 实现的,它的编译缓存全权交由 SWC 管理,而 SWC 内部使用文件绝对路径作为缓存 Key。这本不是什么大问题,问题出在 Windows 上——Windows 的文件系统是不区分大小写的,但 SWC 的缓存 Key 逻辑是区分大小写的。
这就造成了一个恐怖的局面:
- 项目路径是
D:\Code\JellyBit.Studio\food\cola - 某个模块被访问时,SWC 生成了一个 Key:
JellyBit.Studio - 但当操作系统在底层 I/O 中返回了全小写的路径:
jellybit.studio时,SWC 认为这是一个全新的模块 - 于是它重新编译,生成一个新缓存条目
- 旧的缓存条目不会被清理,变成死数据
- 每一次热更新,甚至每一次文件系统响应延迟,都会触发这个“造假过程”
结果就是:缓存越积越多,直到你把所有内存吃完。
我通过 Get-Process node 持续监控,确认了 CPU 几乎一直处于 idle 状态——不是运算压力,而是纯粹的内存泄漏。
更有意思的是,含 . 的路径(比如 JellyBit.Studio)还会被 SWC 解析为模块命名空间分隔符,导致缓存 key 的生成逻辑进一步异常。这相当于在一个原本就脆弱的机制上,再补了一刀。
问题很冷门,但教训很通用
一旦定位到问题,解决方案其实非常克制:
最简单有效的方法:把项目路径改成全小写、kebab-case。
| 之前 | 之后 |
|---|---|
C:\Users\wo\IdeaProjects\JellyBit.Studio\food\cola | C:\Users\wo\IdeaProjects\jellybit-studio\food\cola |
就这么简单。没有改一行代码,没有升一个版本,问题彻底消失。
当然,你也可以选择——不改路径——通过禁用 Turbopack 来规避:
// next.config.js
module.exports = {
turbopack: false,
}