修复 Windows 9P 导致的 Next.js 编译缓慢
这是“Kita 工程案例”系列的第三篇。前两篇建立了 Dev Container 与 DIND PostgreSQL;这一篇处理实际使用后出现的性能和文件所有权问题。
故障现象
Kita 源码位于 Windows,并以 bind mount 出现在:
1 | /workspaces/Kita |
这种方式很适合源码:Windows、VS Code 和容器看到的是同一份文件。
问题出现在 Next.js 开发缓存和依赖目录。首次页面编译达到分钟级,.next 中还曾混入 root 创建的文件,导致 TypeScript 和 next build 产生看似无关的错误。
我最后确认这是两个问题,不应该用一个模糊的“Docker 很慢”概括:
- Windows 与 Linux 容器之间的小文件 I/O 很慢;
- 不同用户写入同一
.next,破坏了缓存所有权。
为什么 .next 和 node_modules 特别敏感
Next.js 开发模式会在 .next 中频繁创建和读取:
- Turbopack 编译产物;
- 路由与模块缓存;
- 服务端和客户端中间文件;
- 大量体积小、数量多的文件。
node_modules 也包含大量小文件,模块解析会不断访问它们。
Windows bind mount 的优势是实时共享源码,不擅长这种高频 Linux 小文件工作负载。把整个项目移入 volume 又会失去宿主机直接保存源码的便利。
所以修复目标是:只移动真正有问题的两个目录。
第一步:增加两个定向 named volume
在 .devcontainer/devcontainer.json 中加入:
1 | "mounts": [ |
最终文件系统边界是:
1 | Windows bind mount |
源码、文档和配置仍然属于 Windows 仓库;依赖和可再生缓存使用 Docker 所在的 Linux 文件系统。
第二步:修复新 volume 的所有权
新挂载点第一次创建时可能归 root 所有,而 Kita 的日常用户是 node。
因此创建命令使用:
1 | "postCreateCommand": "sudo corepack enable && sudo chown node:node node_modules .next && pnpm install --frozen-lockfile" |
这里只修改两个明确的挂载点,不递归改变整个 workspace。pnpm install 随后在新的 Linux volume 中安装依赖。
修改 mount 后必须 Rebuild Container。只重启终端不会重新建立挂载关系。
第三步:阻止 root 再次污染缓存
只执行一次 chown 不能阻止未来的 root 命令再次写入。
Kita 为主要脚本增加 assert-dev-workspace-user.mjs。它检查:
- bind-mounted workspace 中 UID 不能为 0;
- 执行关键命令前,
.next不能包含其他 owner 的文件; next dev与next build不能同时操作同一输出目录。
package.json 中的入口因此类似:
1 | "build": "node scripts/assert-dev-workspace-user.mjs --check-next --mode=build && next build", |
next.config.ts 还保留第二层 root 防护。这样即使有人绕过 package script 直接启动 Next.js,项目仍会拒绝在 /workspaces 中以 root 运行。
第四步:不要同时运行 dev 和 build
开发服务器和生产构建默认都使用 .next。
如果 next dev 正在写缓存,同时执行 next build,即使用户一致,也可能产生相互覆盖和难以复现的生成错误。
Kita 的守卫会检查活动进程:
1 | 准备 build |
提交前运行 build 时,先停止开发服务器。
验证 volume 是否真正生效
Rebuild Container 后,在容器终端检查:
1 | whoami |
也可以使用:
1 | mount | grep -E '/node_modules|/.next' |
预期:
- 当前用户为
node; - 两个目录是独立 Docker volume 挂载;
- 源码目录仍然是 bind mount;
pnpm install --frozen-lockfile成功;pnpm dev可以写入.next。
怎样测量,而不是凭感觉判断
至少记录三组时间:
- Rebuild 后首次
pnpm dev到首页可用; - 首次打开数据较重的
/games; - 修改一个组件后的增量编译。
Kita 在修复前首屏编译达到分钟级,调整后回到秒级。第一次 /games 仍可能因为路由、Payload 和数据库首次加载较慢,所以不能只用一个页面的一次请求判断 volume 是否有效。
测试时还要区分:
1 | 冷启动 |
否则很容易把不同阶段的等待全部归咎于 Docker。
常见错误
把整个仓库复制进 named volume
这会让宿主机源码、Git 和容器内部文件产生新的事实来源。Kita 只移动依赖与缓存。
用 root 安装依赖
安装成功不代表正确。随后普通用户可能无法更新依赖或缓存,错误会延迟出现。
把数据库 volume 当缓存删除
.next 可以重建,PostgreSQL 数据不应默认删除。处理 Docker volume 前必须先识别其职责。
修复性能时忽略数据库
Kita 当时还存在 PostgreSQL 没有自动启动的问题。页面长时间等待既可能来自编译,也可能来自数据库连接超时。最后同时用 targeted volume 和 pnpm dev:services 分别修复。
回滚方案
如果新的 volume 配置有问题:
- 先停止 Next.js;
- 保留 Windows 源码和未提交改动;
- 回滚
mounts与postCreateCommand; - Rebuild Container;
- 重新安装依赖;
- 再做一次冷启动测量。
不要在没有识别 volume 名称和内容前批量删除 Docker volumes。
当前结果
Kita 目前只有两个经过实测证明的性能例外:node_modules 和 .next。没有为了追求更多隔离把源码、Media 或其他目录继续放进 volume。
下一篇离开开发环境,进入内容存储:怎样配置 Payload Media、Cloudflare R2、图片尺寸、公开域名和生产 fail-fast。















