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();
});
});
});