From 415407d3b8938327886d50e3d822c6fb03851c08 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 9 Sep 2026 13:13:54 +0800 Subject: [PATCH] =?utf8?q?fix:=20=E5=8E=BB=E9=99=A4=20dev=20=E6=A8=A1?= =?utf8?q?=E5=9D=97=E5=93=8D=E5=BA=94=E4=B8=AD=E7=9A=84=E5=86=85=E8=81=94?= =?utf8?q?=20base64=20sourcemap?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 | 46 +++++++++++++++++++++++++ vite.local.config.ts | 60 +++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 scripts/verify-browser-wire.mjs diff --git a/scripts/verify-browser-wire.mjs b/scripts/verify-browser-wire.mjs new file mode 100644 index 0000000..9213dce --- /dev/null +++ b/scripts/verify-browser-wire.mjs @@ -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); diff --git a/vite.local.config.ts b/vite.local.config.ts index 48b0cc9..37967e1 100644 --- a/vite.local.config.ts +++ b/vite.local.config.ts @@ -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()], }); -- 2.43.0