From 54c7b04d384f332ca5e1d1cd1fd3f68c0cf67669 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Fri, 16 Oct 2026 02:03:02 +0000 Subject: [PATCH 1/2] perf: share the compile cache with child processes and worker threads `enableCompileCache()` only covers the current thread, so minimizer workers and other child processes compiled every module from scratch. Setting `NODE_COMPILE_CACHE` to the cache directory hands it to them at startup, unless the environment already names one or disables the cache. --- ...hare-compile-cache-with-child-processes.md | 5 ++ packages/webpack-cli/bin/cli.js | 8 ++- .../build/compile-cache/compile-cache.test.js | 70 +++++++++++++++++++ test/build/compile-cache/src/index.js | 1 + test/build/compile-cache/webpack.config.js | 24 +++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .changeset/share-compile-cache-with-child-processes.md create mode 100644 test/build/compile-cache/compile-cache.test.js create mode 100644 test/build/compile-cache/src/index.js create mode 100644 test/build/compile-cache/webpack.config.js diff --git a/.changeset/share-compile-cache-with-child-processes.md b/.changeset/share-compile-cache-with-child-processes.md new file mode 100644 index 00000000000..12219e104bd --- /dev/null +++ b/.changeset/share-compile-cache-with-child-processes.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": patch +--- + +perf: share the compile cache with child processes and worker threads, such as minimizer workers, so they start warm too diff --git a/packages/webpack-cli/bin/cli.js b/packages/webpack-cli/bin/cli.js index 6e496b42bb9..39d0752f112 100755 --- a/packages/webpack-cli/bin/cli.js +++ b/packages/webpack-cli/bin/cli.js @@ -8,7 +8,13 @@ const { enableCompileCache } = require("node:module"); if (enableCompileCache) { try { - enableCompileCache(); + const { directory } = enableCompileCache(); + + // Minimizer workers and other child processes start without the cache; + // the variable Node reads at startup hands them the same directory. + if (directory && process.env.NODE_COMPILE_CACHE === undefined) { + process.env.NODE_COMPILE_CACHE = directory; + } } catch { // Ignore errors } diff --git a/test/build/compile-cache/compile-cache.test.js b/test/build/compile-cache/compile-cache.test.js new file mode 100644 index 00000000000..d1d10cf05fe --- /dev/null +++ b/test/build/compile-cache/compile-cache.test.js @@ -0,0 +1,70 @@ +"use strict"; + +const fs = require("node:fs"); +const { enableCompileCache } = require("node:module"); +const os = require("node:os"); +const path = require("node:path"); +const { run } = require("../../utils/test-utils"); + +/** + * Runs the CLI on a configuration that reports the compile cache directory of the + * child process and the worker thread it starts. + * @param {Record} env variables to set on top of a clean environment + * @returns {Promise<{ variable: string | null, child: string | null, worker: string | null }>} what it reported + */ +const report = async (env) => { + const cleanEnv = Object.fromEntries( + Object.entries(process.env).filter( + ([name]) => !name.startsWith("NODE_COMPILE_CACHE") && name !== "NODE_DISABLE_COMPILE_CACHE", + ), + ); + const { exitCode, stdout } = await run(__dirname, [], { + env: { ...cleanEnv, ...env }, + extendEnv: false, + }); + + expect(exitCode).toBe(0); + + return JSON.parse(stdout.slice(stdout.indexOf("REPORT") + 6).split("\n")[0]); +}; + +describe("compile cache", () => { + // `module.enableCompileCache` is only available on Node.js >= 22.8.0 + if (!enableCompileCache) { + it("should hand nothing on without a compile cache", async () => { + expect(await report({})).toEqual({ variable: null, child: null, worker: null }); + }); + + return; + } + + it("should hand the compile cache to child processes and worker threads", async () => { + const { variable, child, worker } = await report({}); + + expect(typeof variable).toBe("string"); + expect(child.startsWith(variable)).toBe(true); + expect(worker.startsWith(variable)).toBe(true); + }); + + it("should keep a directory the environment already names", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "webpack-cli-compile-cache-")); + + try { + const { variable, child, worker } = await report({ NODE_COMPILE_CACHE: directory }); + + expect(variable).toBe(directory); + expect(child.startsWith(directory)).toBe(true); + expect(worker.startsWith(directory)).toBe(true); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it("should hand nothing on where the compile cache is disabled", async () => { + expect(await report({ NODE_DISABLE_COMPILE_CACHE: "1" })).toEqual({ + variable: null, + child: null, + worker: null, + }); + }); +}); diff --git a/test/build/compile-cache/src/index.js b/test/build/compile-cache/src/index.js new file mode 100644 index 00000000000..94ea297bb85 --- /dev/null +++ b/test/build/compile-cache/src/index.js @@ -0,0 +1 @@ +console.log("compile cache"); diff --git a/test/build/compile-cache/webpack.config.js b/test/build/compile-cache/webpack.config.js new file mode 100644 index 00000000000..769c928225c --- /dev/null +++ b/test/build/compile-cache/webpack.config.js @@ -0,0 +1,24 @@ +"use strict"; + +const { execFileSync } = require("node:child_process"); +const { Worker } = require("node:worker_threads"); + +// `module.getCompileCacheDir` is only available on Node.js >= 22.8.0 +const script = "(require('node:module').getCompileCacheDir || (() => null))() || null"; + +const child = execFileSync(process.execPath, ["-p", `JSON.stringify(${script})`], { + encoding: "utf8", +}); + +new Worker(`require("node:worker_threads").parentPort.postMessage(${script})`, { + eval: true, +}).once("message", (worker) => { + process.stdout.write( + `REPORT${JSON.stringify({ + variable: process.env.NODE_COMPILE_CACHE || null, + child: JSON.parse(child), + worker, + })}\n`, + ); + process.exit(0); +}); From 0b7bf17e9922305bf49a6dfafb4cc5846ae9cad8 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Fri, 16 Oct 2026 02:10:14 +0000 Subject: [PATCH 2/2] fix: forward the compile cache only when the CLI enabled it A cache a preload already enabled reports its version-tagged directory, which a child would tag again. Also exit the test config from the write callback so the report survives an asynchronous pipe. --- packages/webpack-cli/bin/cli.js | 13 +++++++++---- test/build/compile-cache/compile-cache.test.js | 11 +++++++++++ test/build/compile-cache/preload.js | 3 +++ test/build/compile-cache/webpack.config.js | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 test/build/compile-cache/preload.js diff --git a/packages/webpack-cli/bin/cli.js b/packages/webpack-cli/bin/cli.js index 39d0752f112..0ebc85ef4ac 100755 --- a/packages/webpack-cli/bin/cli.js +++ b/packages/webpack-cli/bin/cli.js @@ -4,15 +4,20 @@ // `module.enableCompileCache` is only available on Node.js >= 22.8.0 // eslint-disable-next-line n/no-unsupported-features/node-builtins -const { enableCompileCache } = require("node:module"); +const { constants, enableCompileCache } = require("node:module"); if (enableCompileCache) { try { - const { directory } = enableCompileCache(); + const { status, directory } = enableCompileCache(); // Minimizer workers and other child processes start without the cache; - // the variable Node reads at startup hands them the same directory. - if (directory && process.env.NODE_COMPILE_CACHE === undefined) { + // the variable Node reads at startup hands them the same directory. Only a + // fresh start returns the base directory, which children tag by version. + if ( + // eslint-disable-next-line n/no-unsupported-features/node-builtins + status === constants.compileCacheStatus.ENABLED && + process.env.NODE_COMPILE_CACHE === undefined + ) { process.env.NODE_COMPILE_CACHE = directory; } } catch { diff --git a/test/build/compile-cache/compile-cache.test.js b/test/build/compile-cache/compile-cache.test.js index d1d10cf05fe..f094e7871e8 100644 --- a/test/build/compile-cache/compile-cache.test.js +++ b/test/build/compile-cache/compile-cache.test.js @@ -60,6 +60,17 @@ describe("compile cache", () => { } }); + it("should hand nothing on when a preload has already enabled the compile cache", async () => { + const { variable, child, worker } = await report({ + NODE_OPTIONS: `--require ${path.resolve(__dirname, "preload.js")}`, + }); + + // The preload's directory is already version-tagged; handing it on would tag it twice. + expect(variable).toBeNull(); + expect(child).toBe(worker); + expect(child.match(/v\d+\.\d+\.\d+-/g)).toHaveLength(1); + }); + it("should hand nothing on where the compile cache is disabled", async () => { expect(await report({ NODE_DISABLE_COMPILE_CACHE: "1" })).toEqual({ variable: null, diff --git a/test/build/compile-cache/preload.js b/test/build/compile-cache/preload.js new file mode 100644 index 00000000000..a5c3cad7ca5 --- /dev/null +++ b/test/build/compile-cache/preload.js @@ -0,0 +1,3 @@ +"use strict"; + +require("node:module").enableCompileCache(); diff --git a/test/build/compile-cache/webpack.config.js b/test/build/compile-cache/webpack.config.js index 769c928225c..ba8979a855a 100644 --- a/test/build/compile-cache/webpack.config.js +++ b/test/build/compile-cache/webpack.config.js @@ -19,6 +19,6 @@ new Worker(`require("node:worker_threads").parentPort.postMessage(${script})`, { child: JSON.parse(child), worker, })}\n`, + () => process.exit(0), ); - process.exit(0); });