From 4d9c32a84bf80c3e30c382235e63fca0da08aff7 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 26 Oct 2026 02:36:45 -0700 Subject: [PATCH] perf(build): keep bundled scripts one-byte in V8 to cut idle memory (#33786) V8 stores a script source as UTF-16 when any character is above U+00FF, so a single em dash in a hot chunk doubled that chunk's resident source. Emit ASCII (escaped) output for main, preload, renderer and worker bundles, escape the CJK sentence marks in the prose URL regex source, and fail the renderer boot-graph check if a boot chunk regresses to two-byte. Co-authored-by: m4air --- config/scripts/renderer-boot-graph.mjs | 16 ++++++++++++++++ electron.vite.config.ts | 12 +++++++++++- src/shared/prose-url-boundary.ts | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/config/scripts/renderer-boot-graph.mjs b/config/scripts/renderer-boot-graph.mjs index a0168cb8f1b..d99c5468e78 100644 --- a/config/scripts/renderer-boot-graph.mjs +++ b/config/scripts/renderer-boot-graph.mjs @@ -84,6 +84,13 @@ export function findForbiddenBootPayloads(rendererDir, payloads) { return violations } +// V8 keeps a whole script as UTF-16 when any char is above U+00FF, doubling that boot chunk's memory. +export function findTwoByteBootChunks(rendererDir) { + return readRendererBootGraph(rendererDir) + .chunks.filter((chunk) => /[\u0100-\uffff]/.test(fs.readFileSync(chunk.file, 'utf8'))) + .map((chunk) => path.basename(chunk.file)) +} + export function verifyRendererBootGraph(root = process.cwd()) { const rendererDir = path.join(root, RENDERER_BUILD_DIR) const { chunks, totalBytes } = readRendererBootGraph(rendererDir) @@ -91,6 +98,15 @@ export function verifyRendererBootGraph(root = process.cwd()) { console.log( `Renderer boot graph: ${chunks.length} chunks, ${(totalBytes / 1024).toFixed(1)} KB minified.` ) + const twoByteChunks = findTwoByteBootChunks(rendererDir) + if (twoByteChunks.length > 0) { + console.error('Boot chunks contain chars above U+00FF that output.charset cannot escape:') + for (const chunk of twoByteChunks) { + console.error(` ${chunk}`) + } + console.error('Escape them in the source (e.g. \\u2014 inside String.raw regex sources).') + return 1 + } if (violations.length === 0) { return 0 } diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5c5f98f9322..98bb16bd983 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -227,6 +227,8 @@ export const electronViteConfig: UserConfig = { preserveEntrySignatures: 'exports-only', // Electron and sidecar launchers require stable CommonJS paths despite Rolldown’s SSR ESM default. output: { + // V8 stores a whole script as UTF-16 when any char is above U+00FF; escaping keeps sources one-byte (half the memory). + charset: 'ascii', format: 'cjs', entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js' @@ -260,6 +262,10 @@ export const electronViteConfig: UserConfig = { build: { externalizeDeps: { exclude: ['zod'] + }, + rollupOptions: { + // Unminified preload keeps dependency JSDoc whose em dashes would force two-byte storage in every renderer. + output: { charset: 'ascii', comments: { legal: true, annotation: true, jsdoc: false } } } } }, @@ -284,7 +290,10 @@ export const electronViteConfig: UserConfig = { createViteCacheRetentionPlugin() ], worker: { - format: 'es' + format: 'es', + rollupOptions: { + output: { charset: 'ascii' } + } }, build: { manifest: true, @@ -296,6 +305,7 @@ export const electronViteConfig: UserConfig = { rollupOptions: { // Shared chunks must not import HTML entries that mount another React root. preserveEntrySignatures: 'strict', + output: { charset: 'ascii' }, input: { index: resolve('src/renderer/index.html'), popout: resolve('src/renderer/popout.html'), diff --git a/src/shared/prose-url-boundary.ts b/src/shared/prose-url-boundary.ts index 0f12c31bdf6..f7071462576 100644 --- a/src/shared/prose-url-boundary.ts +++ b/src/shared/prose-url-boundary.ts @@ -1,6 +1,7 @@ // Why not all non-ascii punctuation: titles keep `・`, `–` and `·` unencoded in a url // (`/wiki/ジョン・スミス`), so only brackets, quotes, spaces and sentence marks end one. -const PROSE_URL_TERMINATOR_SOURCE = String.raw`(?![\0-\x7f])[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Z}、。,.!?:;。、…‥—]` +// Escaped CJK sentence marks 、。,.!?:;。、…‥— keep this module's chunk one-byte in V8. +const PROSE_URL_TERMINATOR_SOURCE = String.raw`(?![\0-\x7f])[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Z}\u3001\u3002\uFF0C\uFF0E\uFF01\uFF1F\uFF1A\uFF1B\uFF61\uFF64\u2026\u2025\u2014]` const NOT_PROSE_URL_TERMINATOR = `(?!${PROSE_URL_TERMINATOR_SOURCE})`