From 409e84b06946e7e4bb0fd81b21c399d81ab20444 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:03:44 +0300 Subject: [PATCH 1/3] perf: reduce asset reporting and broadcast overhead --- .changeset/server-reporting-overhead.md | 5 +++++ lib/Server.js | 28 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 .changeset/server-reporting-overhead.md diff --git a/.changeset/server-reporting-overhead.md b/.changeset/server-reporting-overhead.md new file mode 100644 index 0000000000..7a91a3eea4 --- /dev/null +++ b/.changeset/server-reporting-overhead.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-server": patch +--- + +Collect only the statistics needed by the asset report, reuse directory-listing middleware, and serialize each WebSocket broadcast only once. diff --git a/lib/Server.js b/lib/Server.js index 38577856c9..e68754ae98 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -2171,14 +2171,21 @@ class Server { '', ); + const jsonStats = /** @type {Stats | MultiStats} */ (stats).toJson({ + all: false, + children: true, + assets: true, + publicPath: true, + }); + /** * @type {StatsCompilation[]} */ const statsForPrint = typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined" ? /** @type {NonNullable} */ - (/** @type {MultiStats} */ (stats).toJson().children) - : [/** @type {Stats} */ (stats).toJson()]; + (jsonStats.children) + : [jsonStats]; res.write("

Assets Report:

"); @@ -2411,6 +2418,11 @@ class Server { for (const staticOption of staticOptions) { for (const publicPath of staticOption.publicPath) { if (staticOption.serveIndex) { + const serveIndexMiddleware = serveIndex( + staticOption.directory, + staticOption.serveIndex, + ); + middlewares.push({ name: "serve-index", path: publicPath, @@ -2426,11 +2438,7 @@ class Server { return next(); } - serveIndex( - staticOption.directory, - /** @type {ServeIndexOptions} */ - (staticOption.serveIndex), - )(req, res, next); + serveIndexMiddleware(req, res, next); }, }); } @@ -3339,10 +3347,14 @@ class Server { * @param {EXPECTED_ANY=} params params */ sendMessage(clients, type, data, params) { + /** @type {string | undefined} */ + let message; + for (const client of clients) { // `ws` uses `WebSocket.OPEN`, which is `1` if (client.readyState === 1) { - client.send(JSON.stringify({ type, data, params })); + message ??= JSON.stringify({ type, data, params }); + client.send(message); } } } From ecad4fe41607ecfc1ad3a5432da9cff68102ec6f Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:54:09 +0200 Subject: [PATCH 2/3] Avoid child stats work for single compilers --- lib/Server.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index e68754ae98..587a2006ea 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -2171,9 +2171,11 @@ class Server { '', ); + const isMultiStats = + typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined"; const jsonStats = /** @type {Stats | MultiStats} */ (stats).toJson({ all: false, - children: true, + children: isMultiStats, assets: true, publicPath: true, }); @@ -2181,11 +2183,10 @@ class Server { /** * @type {StatsCompilation[]} */ - const statsForPrint = - typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined" - ? /** @type {NonNullable} */ - (jsonStats.children) - : [jsonStats]; + const statsForPrint = isMultiStats + ? /** @type {NonNullable} */ + (jsonStats.children) + : [jsonStats]; res.write("

Assets Report:

"); From fc2f57994a8fc2e783856d81b3001aab346d857d Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:54:12 +0200 Subject: [PATCH 3/3] Test compiler-specific asset report stats --- test/e2e/built-in-routes.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/e2e/built-in-routes.test.js b/test/e2e/built-in-routes.test.js index 9432839afb..544c74fe77 100644 --- a/test/e2e/built-in-routes.test.js +++ b/test/e2e/built-in-routes.test.js @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, it } from "node:test"; import { expect } from "expect"; +import { spyOn } from "jest-mock"; import webpack from "webpack"; import Server from "../../lib/Server.js"; import config from "../fixtures/client-config/webpack.config.js"; @@ -61,6 +62,11 @@ describe("Built in routes", () => { }); it("should handle GET request to directory index and list all middleware directories", async (t) => { + const stats = await new Promise((resolve) => { + server.middleware.waitUntilValid(resolve); + }); + const toJsonSpy = spyOn(stats, "toJson"); + page .on("console", (message) => { consoleMessages.push(message); @@ -85,6 +91,14 @@ describe("Built in routes", () => { t.assert.snapshot(consoleMessages.map((message) => message.text())); t.assert.snapshot(pageErrors); + + expect(toJsonSpy).toHaveBeenCalledWith({ + all: false, + children: false, + assets: true, + publicPath: true, + }); + toJsonSpy.mockRestore(); }); it("should handle HEAD request to directory index", async (t) => { @@ -191,6 +205,11 @@ describe("Built in routes", () => { }); it("should handle GET request to directory index and list all middleware directories", async (t) => { + const stats = await new Promise((resolve) => { + server.middleware.waitUntilValid(resolve); + }); + const toJsonSpy = spyOn(stats, "toJson"); + page .on("console", (message) => { consoleMessages.push(message); @@ -215,6 +234,14 @@ describe("Built in routes", () => { t.assert.snapshot(consoleMessages.map((message) => message.text())); t.assert.snapshot(pageErrors); + + expect(toJsonSpy).toHaveBeenCalledWith({ + all: false, + children: true, + assets: true, + publicPath: true, + }); + toJsonSpy.mockRestore(); }); }); });