]> acesimba.cloud Git - StationBuilder.git/commitdiff
fix: 去除 dev 模块响应中的内联 base64 sourcemap
authorroot <root@localhost.localdomain>
Wed, 9 Sep 2026 05:13:54 +0000 (13:13 +0800)
committerroot <root@localhost.localdomain>
Wed, 9 Sep 2026 05:13:54 +0000 (13:13 +0800)
vinext RSC 管线会给 use client 依赖(antd.js)内联 7MB sourcemap,使响应从 3MB
膨胀到 9.66MB;经 nginx gzip 后仍占 2.28MB,为首屏传输主体。新增 vite 中间件
插件将 data URI sourcemap 替换为外部 .map 引用。实测:首屏总传输 3.62MB→
1.66MB,antd.js 2.28MB→674KB,页面加载 10.8s→5.0s,无强制 reload。

scripts/verify-browser-wire.mjs [new file with mode: 0644]
vite.local.config.ts

diff --git a/scripts/verify-browser-wire.mjs b/scripts/verify-browser-wire.mjs
new file mode 100644 (file)
index 0000000..9213dce
--- /dev/null
@@ -0,0 +1,46 @@
+import { chromium } from "playwright";
+
+const PUB = "https://aidemo.istation.cloud/t/stationbuilder/";
+const browser = await chromium.launch();
+const ctx = await browser.newContext();
+const page = await ctx.newPage();
+const cdp = await ctx.newCDPSession(page);
+await cdp.send("Network.enable");
+const wire = new Map(); // url -> {encoded, enc}
+const reqUrl = new Map(); // requestId -> url
+cdp.on("Network.responseReceived", (e) => {
+  reqUrl.set(e.requestId, e.response.url);
+  wire.set(e.response.url, {
+    encoded: 0,
+    enc: e.response.headers["content-encoding"] || "-",
+  });
+});
+cdp.on("Network.loadingFinished", (e) => {
+  const u = reqUrl.get(e.requestId);
+  if (u && wire.has(u)) wire.get(u).encoded = e.encodedDataLength || 0;
+});
+let reloads = 0;
+page.on("domcontentloaded", () => reloads++);
+const t0 = Date.now();
+await page.goto(PUB, { waitUntil: "commit", timeout: 60000 });
+await page.waitForSelector(".company-side-nav", { timeout: 60000 });
+await page.waitForFunction(() => {
+  const els = Array.from(document.querySelectorAll(".empty-state"));
+  return !els.some(e => /加载中/.test(e.textContent || ""));
+}, undefined, { timeout: 60000 });
+const loadMs = Date.now() - t0;
+// keep page open 20s to detect a forced full-reload
+await page.waitForTimeout(20000);
+let total = 0, top = [];
+for (const [u, v] of wire) {
+  total += v.encoded;
+  top.push([v.encoded, v.enc, u.replace(PUB, "")]);
+}
+top.sort((a, b) => b[0] - a[0]);
+console.log(`loaded in ${loadMs}ms; domcontentloaded count (reload detector): ${reloads}`);
+console.log(`TOTAL wire bytes (CDP): ${(total / 1048576).toFixed(2)}MB across ${wire.size} responses`);
+console.log("top 8 by wire:");
+for (const [b, enc, u] of top.slice(0, 8))
+  console.log(`  ${(b / 1024).toFixed(0)}KB [${enc}] ${u.slice(0, 80)}`);
+await browser.close();
+process.exit(0);
index 48b0cc9a3a3f90d308b6961ffc475a4f49f429c6..37967e1d3d70d7b4d7ef1b73942233d77fcbac26 100644 (file)
@@ -1,5 +1,61 @@
 import vinext from "vinext";
-import { defineConfig } from "vite";
+import { defineConfig, type Plugin } from "vite";
+
+/**
+ * Strip inline base64 sourcemaps from dev module responses.
+ * vinext's RSC pipeline inlines sourcemaps into "use client" deps (e.g. antd.js
+ * balloons from 3MB to ~10MB), dominating first-load transfer. Replace the data
+ * URI with an external .map reference. Only buffers module-like URLs; never
+ * touches /api/ (streaming) or document/RSC responses.
+ */
+function stripInlineSourcemap(): Plugin {
+  return {
+    name: "strip-inline-sourcemap",
+    configureServer(server) {
+      server.middlewares.use((req, res, next) => {
+        const url = String(req.url || "");
+        const moduleLike =
+          !url.includes("/api/") &&
+          (url.includes("/node_modules/") || url.includes("/@id") ||
+           url.includes("/@vite") || url.includes("/@react-refresh") ||
+           /\.(tsx?|jsx?|css)(\?|$)/.test(url));
+        if (!moduleLike) return next();
+        const origWrite = res.write.bind(res);
+        const origEnd = res.end.bind(res);
+        const chunks: Buffer[] = [];
+        (res as any).write = (chunk: any) => {
+          if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+          return true;
+        };
+        (res as any).end = (chunk?: any) => {
+          try {
+            if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+            let body = Buffer.concat(chunks);
+            const ct = String(res.getHeader("content-type") || "");
+            if (ct.includes("javascript") && body.includes("sourceMappingURL=data:")) {
+              const mapName = url.split("?")[0].split("/").pop() + ".map";
+              body = Buffer.from(
+                body.toString("utf8").replace(
+                  /\n?\/\/# sourceMappingURL=data:application\/json[^;\n]*;base64,[A-Za-z0-9+/=]+/,
+                  "\n//# sourceMappingURL=" + mapName
+                )
+              );
+              res.removeHeader("transfer-encoding");
+              res.setHeader("content-length", body.length);
+            }
+            (res as any).write = origWrite;
+            (res as any).end = origEnd;
+            if (body.length) origWrite(body);
+            origEnd();
+          } catch {
+            origEnd();
+          }
+        };
+        next();
+      });
+    },
+  };
+}
 
 /**
  * Self-contained local UI configuration.
@@ -27,5 +83,5 @@ export default defineConfig({
       },
     },
   },
-  plugins: [vinext()],
+  plugins: [vinext(), stripInlineSourcemap()],
 });