Skip to content

perf(frontend): break down the startup bundle and bound frontend leaks - #829

Merged
Neonforge98 merged 1 commit into
developfrom
perf/frontend-bundle-and-leaks
Aug 17, 2026
Merged

perf(frontend): break down the startup bundle and bound frontend leaks#829
Neonforge98 merged 1 commit into
developfrom
perf/frontend-bundle-and-leaks

Conversation

@Harry19081

Copy link
Copy Markdown
Member

Summary

Frontend RAM pass, first batch: break down the startup bundle, fix the real cause of the duplicated-vendor chunks, and bound the frontend growth paths that were leaking — all behavior-preserving except the terminal mount window (see risks). Ranked audit with measured numbers lives in docs/memory-audit-2026-08-16/ram-optimization-findings.md.

Measured with webpack --mode production at HEAD before/after:

Before After
Synchronous initial JS (index.html scripts) 8.37 MB (main 4.74 + vendors 3.73) 0.98 MB (main 0.32 + vendors 0.65)
JS loaded at boot (initial + App import set) ≈ 8.4 MB 5.9 MB (−29 %)
Vendors loaded at boot 3.73 MB ≈ 1.65 MB
Modules emitted in >1 chunk 1 601 / 9.0 MB redundant 339 / 1.9 MB
Total build/ 55 MB 48 MB

Problem

  • src/index.tsx chose between the dev-only webpackMode: "eager" App import and the normal one via an isDev const. Webpack can't fold that, walks both arms, and "eager" wins → production shipped App (and every vendor only App needs) inlined into main.js as ~8 MB of synchronous startup JS.
  • The chat-projection web worker statically imported the registry barrel, which re-exports rendering/registry/events/index.ts and its lazy renderer import()s. That made 2 388 files part of the worker's chunk graph; since the worker entry has no vendors, webpack duplicated react-dom / xterm / CodeMirror / zod / sql-formatter into every shared async chunk (~9 MB).
  • Four sidebar files, SidebarDialogs, ModalSystem, ActionSystemContext and the boot-registered editor actions imported barrels that dragged xterm + addons, all of CodeMirror + 10 language packs, sql-formatter, refractor (for a modal nobody renders) and framer-motion into the boot graph.
  • Several frontend structures grew without bound: unsavedContentCache (two full copies of file text per edited file, never evicted), runtimeCounters.durations[] (one entry per RPC, no drain call site), search "load more" accumulation, per-session todo slots, output channels (+ a sessionStorage rewrite of every channel on every appended line), and every initialized terminal staying mounted forever (incl. all restored at boot).

Solution

  • Inline process.env.NODE_ENV === "development" at the App import branch.
  • Move CONTEXT_CONFIG + chat-config helpers to registry/events/contextConfig.ts (pure data); ActionRegistry/registryAccessors import from it. Worker reach with async edges 2 388 → 62 files. splitChunks unchanged.
  • Deep imports at the boot gateways; Message toast renderer split into a React.lazy MessageContainer (API unchanged); EditorService loaded on first editor-action invocation.
  • Bounds: unsavedContentCache drops the never-read originalContent, tracks dirty, evicts only clean entries past 32 (dirty = truly unsaved edits are never evicted); runtimeCounters running sum/count; searchAppendResultsAtom enforces the documented 20 000-match ceiling; sessionTodoMapAtom LRU 64 (never the active session; rebuilt from events by useTodoSync); useOutputChannels capped at 16 channels with a debounced sessionStorage mirror.
  • TerminalCore keeps the active pane + 4 most-recently-active mounted (terminalMountWindow.ts); colder panes unmount and reattach through the existing attach_pty_stream + serialized-buffer restore path.
  • Regression guards: src/test/staticImportGraph.ts (fast static import walker) backs startupGraph.test.ts (heavy packages / feature trees stay out of the boot graph, eager import stays foldable) and workerStaticGraph.test.ts (worker never reaches the renderer loaders or react-dom again).

Potential risks

  • Terminal mount window (only behavior change): switching to a terminal that has been cold for 5+ activations remounts xterm — brief blank, scroll position/selection reset, and if output arrived while it was cold the restore is the Rust-side bounded snapshot rather than full scrollback. Browser-tab webviews were deliberately not touched (a prototype was reverted; documented in the audit).
  • First toast loads its chunk lazily (local file, sub-ms in Tauri; auto-dismiss timers start on mount so no shortened lifetime).
  • Editor actions (Go to line / Find / Undo …) do a one-time dynamic import of EditorService on first use.
  • The static-graph guards are regex-based (not a full resolver); they'll fail loudly with the import chain if a barrel re-export sneaks a heavy lib back in — bump the allow-list with a reason if that's ever intended.

Validation / Test plan

  • pnpm typecheck clean; eslint clean on all touched files (pre-commit lint-staged passed).
  • pnpm test: full suite green before the last test additions (1104 files / 8696 tests); new suites green: startup/worker graph guards, Message lazy container, cache/todo/search/output-channel bounds, terminal mount window, contextConfig boundary, lazy EditorService.
  • Production builds before/after (table above); module-id duplication measured with an acorn walk over build/.
  • Manual: open >5 terminals, cycle through them (reattach + restore), trigger toasts, Go-to-line/Find/Undo from the palette, sidebar rename/share dialogs, quit-confirmation modal.

Bundle:
- Gate the dev-only eager App import on the inline NODE_ENV test so
  production emits App as an async chunk again (was inlined into main.js).
- Split rendering/registry/events CONTEXT_CONFIG into contextConfig.ts so
  the chat-projection worker's static graph no longer reaches the lazy
  React renderer loaders (worker reach 2388 -> 62 files); this removed
  ~9 MB of duplicated vendor modules across async chunks.
- Deep-import at the sidebar/modal/action-system gateways that dragged
  xterm, CodeMirror + language packs, sql-formatter, refractor and
  framer-motion into the startup graph; lazy-load the toast renderer and
  EditorService.

Leaks / bounds (behavior-preserving):
- unsavedContentCache: drop the unused originalContent copy, evict clean
  entries past 32, never evict dirty ones.
- runtimeCounters: running sum/count instead of unbounded durations[].
- searchAppendResultsAtom: enforce the 20k retained-match ceiling.
- sessionTodoMapAtom: LRU 64 (never the active session).
- useOutputChannels: cap 16 channels, debounce sessionStorage mirror.
- TerminalCore: keep active + 4 recent panes mounted, remount the rest via
  the existing PTY attach/restore path.

Tests: bundle-boundary guards (startup graph, worker graph, eager import),
Message lazy container, cache/todo/search/output-channel bounds, terminal
mount window, contextConfig boundary, lazy EditorService.

Docs: docs/memory-audit-2026-08-16 with measured before/after.

Pre-commit hook ran. Total eslint: 0, total circular: 0
@Neonforge98
Neonforge98 merged commit 9ecf0de into develop Aug 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants