fix(build): raise Node heap for local next build to stop OOM/stall (#4171)

Integrated into release/v3.8.29 — raise Node heap for local next build (extends #4104 Docker OOM fix to the native path). Validated: 10/10 tests, typecheck:core, file-size, test-discovery, eslint green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-18 11:23:12 -03:00
committed by GitHub
parent aaa740f7fd
commit 771f1cfac9
2 changed files with 58 additions and 13 deletions

View File

@@ -107,16 +107,32 @@ export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
}
export function resolveNextBuildEnv(baseEnv = process.env) {
return {
const env = {
...baseEnv,
NEXT_PRIVATE_BUILD_WORKER: baseEnv.NEXT_PRIVATE_BUILD_WORKER || "0",
};
// Raise the Node heap for the spawned `next build`. The webpack production pass
// ("Compiling instrumentation" bundles the whole server graph) is the heaviest
// phase and overflows V8's default ~2 GB ceiling on memory-constrained machines,
// stalling/OOMing local `npm run build` (npm-global installs). #4076/#4104 fixed
// this only in the Docker builder stage (ENV NODE_OPTIONS); the local/native path
// was left unprotected. Respect an existing --max-old-space-size (Docker already
// sets one — don't clobber/duplicate) and let OMNIROUTE_BUILD_MEMORY_MB override.
if (!/--max-old-space-size/.test(env.NODE_OPTIONS || "")) {
const heapMb = Number(baseEnv.OMNIROUTE_BUILD_MEMORY_MB) || 4096;
env.NODE_OPTIONS = `${env.NODE_OPTIONS || ""} --max-old-space-size=${heapMb}`.trim();
}
return env;
}
async function resetStandaloneOutput(rootDir = projectRoot, fsImpl = fs) {
// Use the module-level distDir so NEXT_DIST_DIR is respected
const resolvedDistDir =
rootDir === projectRoot ? distDir : path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next");
rootDir === projectRoot
? distDir
: path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next");
const standaloneRoot = path.join(resolvedDistDir, "standalone");
if (!(await exists(standaloneRoot))) return;
@@ -128,7 +144,9 @@ async function resetStandaloneOutput(rootDir = projectRoot, fsImpl = fs) {
export async function pruneStandaloneArtifacts(rootDir = projectRoot, fsImpl = fs) {
const resolvedDistDirForPrune =
rootDir === projectRoot ? distDir : path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next");
rootDir === projectRoot
? distDir
: path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next");
const standaloneRoot = path.join(resolvedDistDirForPrune, "standalone");
const pruneTargets = [path.join(standaloneRoot, "_tasks")];
@@ -174,11 +192,9 @@ export async function main() {
const standaloneDir = path.join(distDir, "standalone");
if (result.code === 0 && (await exists(standaloneDir))) {
try {
await fs.cp(
path.join(projectRoot, "docs"),
path.join(standaloneDir, "docs"),
{ recursive: true }
);
await fs.cp(path.join(projectRoot, "docs"), path.join(standaloneDir, "docs"), {
recursive: true,
});
console.log("[build-next-isolated] Copied docs/ to standalone output");
} catch (docsCopyErr) {
console.warn("[build-next-isolated] Non-fatal error copying docs/:", docsCopyErr?.message);
@@ -194,7 +210,9 @@ export async function main() {
}
try {
console.log("[build-next-isolated] Assembling standalone bundle (static + public + natives + extras)...");
console.log(
"[build-next-isolated] Assembling standalone bundle (static + public + natives + extras)..."
);
assembleStandalone({
distDir,
outDir: standaloneDir,
@@ -202,10 +220,7 @@ export async function main() {
copyNatives: true,
});
} catch (assembleErr) {
console.warn(
"[build-next-isolated] Non-fatal error assembling standalone:",
assembleErr
);
console.warn("[build-next-isolated] Non-fatal error assembling standalone:", assembleErr);
}
}
process.exitCode = result.code;

View File

@@ -107,6 +107,36 @@ test("resolveNextBuildEnv forces stable build worker mode unless already provide
assert.equal(preservedEnv.NODE_ENV, "production");
});
// Escalated bug (WhatsApp BR, cmqiuhd7600): a local `npm run build` stalls/OOMs
// during the webpack production pass ("Compiling instrumentation" bundles the whole
// server graph). #4076/#4104 raised the heap only in the Docker builder stage; the
// local/native path (build-next-isolated.mjs → resolveNextBuildEnv) was left on V8's
// default ~2 GB ceiling, so memory-constrained npm-global installs hit the same OOM.
test("resolveNextBuildEnv raises the Node heap for memory-constrained local builds", () => {
const env = resolveNextBuildEnv({ NODE_ENV: "production" });
const match = (env.NODE_OPTIONS ?? "").match(/--max-old-space-size=(\d+)/);
assert.ok(
match,
"local build must set NODE_OPTIONS --max-old-space-size to avoid the webpack-pass OOM"
);
assert.ok(
Number(match[1]) >= 4096,
`build heap default must be >= 4096 MB (the V8 default ~2 GB OOMed); got ${match[1]}`
);
});
test("resolveNextBuildEnv does not clobber an existing --max-old-space-size (Docker)", () => {
const env = resolveNextBuildEnv({ NODE_OPTIONS: "--max-old-space-size=8192" });
const occurrences = (env.NODE_OPTIONS.match(/--max-old-space-size=/g) || []).length;
assert.equal(occurrences, 1, "must not duplicate the heap flag when one is already set");
assert.match(env.NODE_OPTIONS, /--max-old-space-size=8192/);
});
test("resolveNextBuildEnv honors the OMNIROUTE_BUILD_MEMORY_MB override", () => {
const env = resolveNextBuildEnv({ OMNIROUTE_BUILD_MEMORY_MB: "6144" });
assert.match(env.NODE_OPTIONS, /--max-old-space-size=6144/);
});
test("getTransientBuildPaths leaves _tasks in place by default", () => {
const paths = getTransientBuildPaths("/repo", {});