build(desktop): the renderer ships as ES modules, and its bundle shrinks

Merged
CH
5d ago
+81-30

What

The desktop renderer's entry now builds as ES modules (format: "esm", splitting: true) instead of one IIFE, and the three documents that load it (index.html, voice.html, call.html) load it with <script type="module">. Features and lazy loading are unchanged: the whiteboard, file tree, plan editor, and session recorder bundles stay IIFEs, loaded as before.

Bytes loaded up front (gzip level 9): 764,016 → 757,948, which is 6,068 smaller.

before (IIFE)after (ESM)
renderer.js764,016596,817
chunk-Z2BTE7WG.js (static import)n/a161,131
up front764,016757,948

The saving comes from dropping the IIFE wrapper and from freer minification of module scope. esbuild also emits two tiny chunks for dynamic import()s that were already in a dependency's code (highlighted-body-*.js, 649 B, and mermaid-*.js, 129 B). The IIFE build inlined those behind a Promise. Now they're fetched the first time that code runs. That's still asynchronous, as before, so nothing new is lazy-loaded on purpose.

Details

  • Chunk names: esbuild's default [name]-[hash], a content hash. Every shared chunk is named chunk, so a stable [name] would collide. The windows read file://, so caching isn't a factor. dist/ is wiped on every build, so stale chunks can't pile up. The entry keeps its name (renderer.js) through the object form of entryPoints.
  • CSP: script-src 'self' already admits the module entry and its sibling chunks; no policy changed.
  • Loading: windows call loadFile with sandbox, contextIsolation, and webSecurity on. Electron's file:// scheme serves module scripts and their relative imports, and no Electron fuse in the build turns that off. On Linux I loaded the built index.html in Electron 43.3.0 with those web preferences, from the plain dist/ and again from inside an app.asar. In both runs index.html, styles.css, renderer.js, and chunk-*.js loaded with status 200. The renderer code ran and stopped where it reads the preload bridge, which that bare harness doesn't provide.
  • Deferred execution: the script tag was already the last element of <body>, and nothing in the renderer reads document.currentScript, waits on DOMContentLoaded, or checks readyState. Module scripts still run before DOMContentLoaded and load, so main's did-finish-load ordering doesn't change.
  • Session replay's path scrubbing (session-replay.ts) matches the directory the page was loaded from. The chunks sit beside renderer.js in that directory, so their stack frames are covered by the same rule.

Bundle budget

  • scripts/build.mjs reads the renderer build's metafile and measures a bundle as itself plus every chunk it reaches through static imports, at any depth, each gzipped on its own. Chunks reached only by a dynamic import() are left out. Bundles the metafile doesn't describe (the IIFEs) are still one file.
  • The build now fails if any of the three documents loads renderer.js without type="module". A classic script would hit a syntax error on the entry's first import and leave the window blank.
  • Baseline change, a deliberate build change, not drift: renderer/renderer.js goes from 762,061 to 757,948. This lowers the baseline: the new up-front figure is below both the old recorded baseline and what main measures today (764,016). LUKE_UPDATE_BUNDLE_BUDGET=1 rewrites every row, so I kept only the renderer's line. The other bundles have drifted since their baselines were recorded, and this PR doesn't touch their baselines.
  • Checked both ways: with the renderer's baseline set to 600,000 the build fails at 757,948, so the chunk is counted (the entry alone, 596,817, would have passed). With call.html put back to a classic script, the build fails on the new check.

Verification

  • pnpm check:affected: exit 0
  • ./scripts/check.sh: exit 0
  • CI could not verify this. It builds nothing for the Mac and runs no window. This change affects how every window loads its script, so a ./scripts/verify.sh run on a Mac is still owed: open the panel, the voice window, and the call window, and confirm each one draws. The owner chose to merge on green and fix anything it turns up afterwards; until that run happens, the release rehearsal on a tag is the first Mac gate this change meets.

šŸ¤– Generated with Claude Code


Open workspace in Conductor


Devin Review


Summary by cubic

The desktop renderer now builds and loads as ES modules instead of one IIFE, shrinking the gzipped bytes loaded up front from 764,016 to 757,948. The index.html, voice.html, and call.html documents load the entry with <script type="module">; the whiteboard, file tree, plan editor, and session recorder bundles are unchanged.

Build and budget

  • The renderer build now emits a module entry plus content-hashed chunks, which the windows load via relative imports. No CSP or Electron fuse changes were needed.
  • The bundle budget now measures a bundle as itself plus every chunk it reaches through static imports, each gzipped on its own; dynamically imported chunks are excluded.
  • The renderer baseline drops to 757,948, and the build fails if any of the three documents loads renderer.js without type="module", since a classic script would hit a syntax error on the entry's first import.