next build 触发 instrumentation 噪声:模块级去重为什么会失效
某次 Docker 构建时,我盯着 CI 日志发现同一行警告像复读机一样刷了将近二十遍:
[logger] INGEST_URL/INGEST_KEY 未配 · 后续 server-side 错误不会上报
[logger] INGEST_URL/INGEST_KEY 未配 · 后续 server-side 错误不会上报
... (还有十几行) ...
Generating static pages (0/47) ...
[logger] INGEST_URL/INGEST_KEY 未配 · 后续 server-side 错误不会上报
这个警告本身是正确的——INGEST_KEY 这类运行时密钥我们刻意不打进镜像,通过 docker-compose 的 env_file 在容器启动时注入。但构建日志看起来像是配置错了,信噪比极差。
现象
next build(无论是本地构建还是 CI 里的 docker build)期间,instrumentation.ts 里注册的初始化逻辑被多次触发,每次都走到"环境变量未配"的警告分支,且代码里已有的模块级去重 flag 完全没有起作用:
let warnedOnce = false
export function log(rec) {
if (!INGEST_KEY) {
if (!warnedOnce) {
console.warn('[logger] INGEST_KEY 未配...')
warnedOnce = true
}
return
}
// ...
}warnedOnce 设为 true 之后,下一条警告依然照常出现。
根因
两件事叠加在一起:
第一:next build 会启动临时 Next 服务
next build 的 "Collecting page data" 阶段并不只是编译代码。它会在构建器内部启动一个临时的 Next 服务,用来执行每个静态页的数据收集。这个临时服务同样会触发 instrumentation.ts 的 register() 钩子。也就是说,register() 不只在 next start 时运行,next build 期间也会触发。
第二:静态页收集是多 worker 并行的,每个 worker 是独立 Node 进程
Next 14 的静态页收集器按 CPU 核数开多个 worker 并行处理页面。每个 worker 是一个独立的 Node 进程,模块状态完全隔离——也就是说,let warnedOnce = false 在每个 worker 里各有一份副本,互不可见。
let flag = false / Map / Set 这类模块级单例在 Next.js 里是每个 worker 一份,不是每次部署一份。next build 有多少个静态页 worker,warnedOnce 就被重置多少次。next dev HMR 每次代码变更也会重启服务,同样如此。Vercel serverless 冷启动也是同一个坑。
正解
用 process.env.NEXT_PHASE 检测当前是否处于构建阶段,然后直接 early-return。日志上报是运行时概念,构建期间什么都不需要做。
const INGEST_URL = process.env.INGEST_URL
const INGEST_KEY = process.env.INGEST_KEY
// next build 的静态页收集阶段同样会触发 instrumentation.register()
// 构建时没有 INGEST_KEY 是预期行为,不是配置错误
const IN_BUILD_PHASE = process.env.NEXT_PHASE === 'phase-production-build'
let dropWarned = false
export async function serverLog(rec) {
if (IN_BUILD_PHASE) return // ← 这一行解决全部问题
if (!INGEST_URL || !INGEST_KEY) {
if (!dropWarned) {
console.warn('[logger] INGEST_KEY 未配...')
dropWarned = true
}
return
}
// ... 正常上报 ...
}process.env.NEXT_PHASE 由 Next.js 在任何用户代码运行前写入,可以放心在模块顶层读取(模块加载时一次性解析):
| 值 | 时机 |
|---|---|
| phase-production-build | next build |
| phase-production-server | next start 生产服务 |
| phase-development-server | next dev |
只需对 phase-production-build early-return。phase-development-server 保留正常警告行为反而有用——它会提醒你本地 .env.local 还没配好。
NODE_ENV=production 在 next build 和 next start 时都成立,无法区分二者。NEXT_PHASE 才是可靠的信号。同一个 module 里有多个运行时副作用(DB ping、Redis 初始化、遥测上报),可以统一提取:
const IS_RUNTIME = process.env.NEXT_PHASE !== 'phase-production-build'
export function pingDb() { if (!IS_RUNTIME) return; /* ... */ }
export function reportTelemetry() { if (!IS_RUNTIME) return; /* ... */ }验证
改动前后用 grep 计数:
# 改前:输出 18(或更多,取决于静态页数量 × worker 数)
docker build . 2>&1 | grep -c 'INGEST_KEY 未配'
# 改后:输出 0
docker build . 2>&1 | grep -c 'INGEST_KEY 未配'同时验证运行时行为未受影响——不带 NEXT_PHASE 直接跑 Node,警告依然出现一次:
node -e "import('./lib/logger.mjs').then(m => m.serverLog({msg:'test'}))"
# → "[logger] INGEST_KEY 未配..." ✓ 运行时警告保留一句话外卖
Next.js 的
instrumentation.register()在构建期间也会触发,且多 worker 架构让模块级去重完全失效——只有NEXT_PHASE === 'phase-production-build'才能精准区分构建和运行时。