Skip to content

Commit cb24a16

Browse files
committed
perf(webpack-cli): stop retaining serve option arrays for the whole session
The serve command stashed the full webpack and dev-server option arrays (~900KB) in its command context, which lives for the entire dev-server session even though the arrays are only needed during setup. Build the arrays transiently in the `options` callback for registration, and derive the action's lightweight lookups (built-in option name set, dev-server arg metadata) from the cached `getArguments` map instead. The large arrays are now reclaimable after startup. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF
1 parent 10cdeeb commit cb24a16

2 files changed

Lines changed: 19 additions & 27 deletions

File tree

.changeset/fast-pumas-cache.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"webpack-cli": patch
33
---
44

5-
Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `<name><ext>` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. This reduces per-invocation CPU work, syscalls, and memory usage.
5+
Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `<name><ext>` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. The `serve` command no longer retains the full option arrays (~900KB) in its context for the whole session, deriving the lookups it needs from the cached argument metadata instead. This reduces per-invocation CPU work, syscalls, and memory usage.

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,16 @@ interface WebpackContext {
114114
webpack: typeof webpack;
115115
}
116116

117-
interface WebpackOptionsContext {
118-
webpackOptions: CommandOption[];
119-
}
120-
121117
interface WebpackDevServerContext {
122118
devServer: typeof import("webpack-dev-server");
123119
}
124120

125-
interface WebpackDevServerOptionsContext {
126-
devServerOptions: CommandOption[];
127-
}
128-
129121
interface KnownWebpackCLICommands {
130122
build: CommandOptions<string[], CommanderArgs, WebpackContext & Context>;
131123
serve: CommandOptions<
132124
string[],
133125
CommanderArgs,
134-
WebpackContext &
135-
WebpackOptionsContext &
136-
WebpackDevServerContext &
137-
WebpackDevServerOptionsContext &
138-
Context
126+
WebpackContext & WebpackDevServerContext & Context
139127
>;
140128
watch: CommandOptions<string[], CommanderArgs, WebpackContext & Context>;
141129
version: CommandOptions<void, CommanderArgs, Context>;
@@ -1651,26 +1639,31 @@ class WebpackCLI {
16511639
dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE],
16521640
preload: async () => {
16531641
const webpack = await this.loadWebpack();
1654-
const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions);
16551642
const devServer = await this.loadWebpackDevServer();
1643+
1644+
return { webpack, devServer };
1645+
},
1646+
options: (cmd) => {
1647+
const { webpack, devServer } = cmd.context;
1648+
const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions);
16561649
// @ts-expect-error different versions of the `Schema` type
16571650
const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, {
16581651
hidden: false,
16591652
negativeHidden: false,
16601653
});
16611654

1662-
return { webpack, webpackOptions, devServer, devServerOptions };
1663-
},
1664-
options: (cmd) => {
1665-
const { webpackOptions, devServerOptions } = cmd.context;
1666-
16671655
return [...webpackOptions, ...devServerOptions];
16681656
},
16691657
action: async (entries: string[], options: CommanderArgs, cmd) => {
1670-
const { webpack, webpackOptions, devServerOptions } = cmd.context;
1658+
const { webpack, devServer } = cmd.context;
16711659
const webpackCLIOptions: Options = { webpack, isWatchingLikeCommand: true };
16721660
const devServerCLIOptions: CommanderArgs = {};
1673-
const webpackOptionNames = new Set(webpackOptions.map((option) => option.name));
1661+
// Derive the built-in option names from the cached argument metadata
1662+
// instead of retaining the full option arrays for the whole session.
1663+
const webpackOptionNames = new Set([
1664+
...this.#CLIOptions.map((option) => option.name),
1665+
...Object.keys(this.#getArguments(webpack, undefined)),
1666+
]);
16741667

16751668
for (const optionName in options) {
16761669
const kebabedOption = this.toKebabCase(optionName);
@@ -1717,9 +1710,8 @@ class WebpackCLI {
17171710
const compilersForDevServer =
17181711
possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]];
17191712
const usedPorts: number[] = [];
1720-
const devServerOptionsByName = new Map(
1721-
devServerOptions.map((option) => [option.name, option]),
1722-
);
1713+
// @ts-expect-error different versions of the `Schema` type
1714+
const devServerArgs = this.#getArguments(webpack, devServer.schema);
17231715

17241716
for (const compilerForDevServer of compilersForDevServer) {
17251717
if (compilerForDevServer.options.devServer === false) {
@@ -1736,10 +1728,10 @@ class WebpackCLI {
17361728
if (name === "argv") continue;
17371729

17381730
const kebabName = this.toKebabCase(name);
1739-
const arg = devServerOptionsByName.get(kebabName);
1731+
const arg = devServerArgs[kebabName];
17401732

17411733
if (arg) {
1742-
args[name] = arg as unknown as WebpackArgument;
1734+
args[name] = arg;
17431735
// We really don't know what the value is
17441736
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17451737
values[name] = options[name as keyof Options] as any;

0 commit comments

Comments
 (0)