Skip to content

Split optional UI on demand and enforce bundle boundaries - #4512

Merged
ymichael merged 22 commits into
mainfrom
bb/audit-project-bundle-size-and-limits-thr_icd5r2cta4
Sep 29, 2026
Merged

ymichael merged 22 commits into
mainfrom
bb/audit-project-bundle-size-and-limits-thr_icd5r2cta4

Conversation

@ymichael

@ymichael ymichael commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

Optional UI was included in startup and thread-route payloads: sidebar footer customization brought dnd-kit back into boot, queued-message reordering and ANSI output were eager, and model-menu and command-palette contents loaded before use. Existing lazy wrappers had inconsistent error handling, and hidden panel mounts downloaded optional code even while closed. Byte limits alone did not prevent those regressions.

What changed

  • Add defineSplit with explicit loading and preload policies, shared import attempts, synchronous cached remounts, local one-line failure UI, and two bounded retries for recognized JavaScript download failures (500 ms and 1.5 seconds).
  • Migrate sidebar customization, secondary panels, timeline diffs and code hosts; split queued messages, terminal output, model-menu contents and command-palette results. Preserve skeletons, close controls, typed input and data/code parallelism.
  • Warm the right-panel shell on workspace-page mount, model contents on workspace-page mount, palette contents at idle, and browser/terminal/new-tab code when the panel opens. The shell also warms shared Git-diff/file-preview code. Keep customization, queue and output demand-driven. Gate first panel/browser mounting and retain state after opening; warming code does not mount hidden views.
  • Add module/package guards and lower raw/Brotli budgets by measured savings. Document the one-time loading-policy audit in docs/ui-split-loading-audit.md.
  • Remove the nine temporary review stories and fixtures, unused scheduler machinery, and runtime preview provider. A small mounted-owner hook now implements the deliberately chosen startup/idle policies. Loading tests now hold/reject actual module imports. Existing feature stories and regression tests remain.

Measured production payloads, in bytes (thread route is additional to boot):

Payload Before raw / Brotli After raw / Brotli New raw / Brotli limit
Boot 1,558,546 / 386,396 1,490,514 / 368,854 1,640,415 / 403,839
Thread route 2,281,610 / 612,858 2,127,341 / 577,767 2,163,849 / 589,508

No protocol or public SDK changes. Retry does not reload the page or rewrite chunk URLs. Chromium can cache a failed module URL, so repeated imports may still require a user reload. CSS-preload, initialization and render failures are not automatically retried. Legacy loaders outside defineSplit retain their existing behavior.

How you verified

  • pnpm exec turbo run build typecheck lint --filter=@bb/app and node apps/app/scripts/check-bundle-budget.mjs passed during integration. Typecheck/lint and the budget check passed again after removing the review stories; lint has 206 existing warnings and no errors.

  • Turbo-scoped focused runs passed: 204 integration tests, 13 final shared-helper tests, and 61 panel-policy tests (overlapping coverage, not an aggregate count). Tests cover cold typing, queue/data overlap, local failure containment, bounded retries and retained first-use gates.

  • Negative controls made imports eager and verified the real budget checker rejected protected modules/packages. Retry and demand-gate tests failed against the original implementations.

  • Production request capture verifies startup panel-shell warming, idle palette warming and concurrent browser/terminal/new-tab downloads on panel opening. Hidden browser/terminal views stay unmounted; xterm and BB code renderers are not downloaded just by warming. Customization, queue and output remain absent from a cold root visit. Child checks covered queue interactions and cold picker/palette input. Real request-abort checks confirmed the Chromium retry limitation.

  • Browser verification used headless Chromium. iOS Safari/native browser views were not reverified; the review proxy now preserves Host, restoring plugin assets/WebSockets. The shared onboarding fixture does not establish full populated-thread behavior.

  • Final cleanup: 88 focused app tests passed, plus 16 tests covering the CI-reported picker setup and palette anatomy failures. Plugin Guide checks passed (34 tests). Build, typecheck, lint and bundle check pass. Independent mutations showed both old and rewritten tests reject missing host fallbacks, broken sidebar controls, dropped picker queries, and unintended toggle-mount preloading. The macOS smoke failure on the previous revision occurred during dependency-cache cleanup (ENOTEMPTY), before package tests.

  • Common-UI warming follow-up: 170 focused tests, build, typecheck, lint and bundle guards pass. Negative controls disabling idle scheduling or panel-open warming fail their behavioral tests. Three matched localhost samples show no palette download on first opening after idle; timing details and screenshots are in the thread-storage handoff. No new byte limits were raised.

  • Page-load model warming: production requests confirm the model menu loads alongside the panel shell before opening. 129 focused tests, build/typecheck/lint and budget guards pass. Fixed the app-2 selection-test timeout by awaiting thread-search module initialization in test setup; a 1.3-second delayed-import experiment fails before and passes after, with the same selection assertions.

AGENT GENERATED

ymichael and others added 22 commits September 29, 2026 10:30
AppSidebar statically imported SidebarFooterCustomize, whose @dnd-kit
imports put all four @dnd-kit packages back in boot-vendor. Load it
with React.lazy behind the Customize footer action, forbid the @dnd-kit
packages in the boot payload, and lower the boot limits by the verified
win (61,053 bytes raw, 15,730 brotli). The thread route keeps @dnd-kit
for queued-message reorder within its existing limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SidebarVisibilityCustomize used a plain React.lazy with a bare
"Loading…" span, so slow or failed downloads showed no header, lost
the Done/Back control and Escape, and a failed import had no retry.

Declare it with defineSplit (render policy, unchanged timing). The card
and compact frames move into SidebarVisibilityCustomizeFrame, shared by
the real editor and its placeholder, so loading and failure keep the
same header, Done/Back button, Escape handling and focus target. The
loading body is a muted "Loading…" that reserves the list's row
height; failure is the shared one-line error with retry.

Guard the implementation module from boot and SplitWorkspaceRoute in
splitBoundaries, add review stories, and preload through the split in
the navigation tests instead of rendering to warm it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The expanded file-change row's diff now loads through the shared split
helper with render-time loading. A failed diff download stays inside
its row as the shared one-line failure instead of unmounting the
timeline. Loading keeps the diff card frame and header height with a
muted "Loading diff…" line in place of skeleton bars.

Guard TimelineFileDiffBlock.tsx out of boot and the SplitWorkspaceRoute
closure, and add a Ladle split review story over real timeline rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Convert the terminal, browser deck, new tab, thread storage tree, and
five file-preview tab wrappers from local lazy/withSuspense machinery to
defineSplit with render-time loading, and remove withSuspense.

- Terminal and new tab show a muted one-line loading message in the tab
  body; file-preview tabs keep the file-preview text skeleton so the
  nested LazyFilePreview load does not swap loading states.
- The browser deck keeps its empty loading fallback and hides its
  failure line while no browser tab is showing, since the deck mounts
  above every other tab's content.
- The thread storage tree keeps its caller-provided fallback.
- Move BrowserTabLifecycleObserver out of BrowserTabDeck so the thread
  route no longer statically imports the deck and BrowserTabContent.
  SplitWorkspaceRoute drops 28,037 bytes raw and about 9 KB brotli.
- Guard all five implementation modules from boot and
  SplitWorkspaceRoute, and add review stories using the real wrappers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SourceCodeHost and DiffHost now load BbSourceCode and BbDiff through
defineSplit instead of module-level React.lazy. The caller's fallback is
still the loading state, and still null for plugin SDK surfaces. A failed
download or render stays inside the host as one red line with a retry.
It no longer escapes and disables a plugin that delegates to Original.
A replacement that never renders Original still does not download BB's
renderer.

BbDiff.tsx and BbSourceCode.tsx join splitBoundaries for boot and
SplitWorkspaceRoute. Code host split review stories cover loading,
failure, and loaded states.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Command rows render ANSI output through ansi-to-html and its legacy
entities tables. Only expanded command details need them, so the
renderer now loads through a defineSplit wrapper when the details
render. The row dispatcher, full-output fetch and preview note stay
eager, so auto-expanded rows never flash and the output fetch does not
wait for the chunk.

SplitWorkspaceRoute closure: -82,496 bytes raw, -22,383 brotli. Boot is
unchanged apart from hash churn. ansi-to-html is now guarded as an
on-demand package, and TerminalOutputBlock as a split boundary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The thread route statically imported the queued-message list, which
pulled @dnd-kit and the list implementation into every thread's first
paint even though most threads have no queue.

LazyQueuedMessagesList now owns the public queue contract (props and
request types) and renders the implementation through defineSplit with
render-time loading, gated on queue presence. An empty queue never
downloads it. The existing data-pending summary warms the same split
while queued-message details load, so data and code fetch in parallel.
Loading keeps the real Queue header and count at the exact drawer
height with simple skeleton bars; the data-pending summary uses the
same body so the two phases do not flip. Failure is the shared one-line
error inside the same card.

Guard QueuedMessagesList.tsx out of boot and SplitWorkspaceRoute, and
forbid the @dnd-kit packages in the SplitWorkspaceRoute closure. The
route closure drops 80,109 bytes raw and 16,428 brotli. Boot grows 334
bytes raw from six extra chunk names in the dependency map.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The trigger, provider tabs, search input, picker state and every
model-picker command registration stay eager. The model list, submenu,
reasoning toggle group, fast-mode switch and handoff action load on
first open, with trigger pointer/focus intent preloading them. Search
typing and arrow-key highlighting work while the menu code loads.

Guard the implementation module out of boot and SplitWorkspaceRoute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep palette.open, thread.search, and plugin command registration plus open
state, query text, recents, and shortcut dispatch in the eager shell. The
ranked command body, settings/plugin-page actions, and PaletteShell now load
through defineSplit when the palette first renders in command mode. The
placeholder keeps a real search input bound to the shell's query, so typing
before the body arrives is preserved, and Escape and command shortcuts work
while it loads. Guard CommandPaletteBody and PaletteShell out of boot and
SplitWorkspaceRoute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael merged commit ebd8150 into main Sep 29, 2026
20 checks passed
@ymichael
ymichael deleted the bb/audit-project-bundle-size-and-limits-thr_icd5r2cta4 branch September 29, 2026 22:10
ymichael added a commit that referenced this pull request Sep 29, 2026
## Human comments

## What was wrong

After #4512, the shared workspace route still imported thread-only
screens, Markdown's HTML parser/sanitizer, and editor extensions that
are always disabled. Plugin SDK registration also imported its embedded
conversation renderer. TipTap's published blockquote bundle contained
another copy of ProseMirror.

## What changed

- Compose the editor's enabled extensions directly, preserving rich-text
gating and extension order. Patch blockquote's ESM/CJS bundles to use
the existing `@tiptap/pm/state` TextSelection instead of bundled copies.
- Download Markdown's HTML pipeline at workspace idle using ordinary
cacheable fetches. Import it only when HTML-enabled Markdown contains
`<`; coordinate fetch/import so first use does not download twice. Plain
Markdown text remains visible while loading or failed; sanitization is
unchanged.
- Split ThreadDetailView and SDK ThreadChat. Thread URLs start the
detail import alongside the workspace route; compose/plugin pages avoid
it. SDK props are unchanged. Existing panel/model/palette preload
policies and automatic import retries remain unchanged.
- Guard the three boundaries, including the plugin runtime closure, and
lower the workspace-route budgets by the measured savings. Document the
download-only pattern.

Production JavaScript, decimal Brotli bytes; overlapping closures are
deduplicated:

| Payload | Merged main | This change |
| --- | ---: | ---: |
| Boot | 369,221 | 369,961 |
| Additional workspace route | 580,544 | 308,314 |
| Initial workspace, including boot | 949,765 | 678,275 |
| Initial actual thread, including boot | 949,765 | 916,608 |
| Workspace plus normal background downloads, including idle HTML |
1,209,250 | 1,085,959 |

These are app JavaScript closures, excluding plugin asset files, CSS,
fonts and user data. Actual thread visits still need the thread code. A
thread that immediately needs HTML totals 962,084 Brotli bytes, 12,319
more than the old combined route because splitting changes compression
and chunk sharing; the largest benefit is on non-thread visits. Normal
warmed execution excludes the 45,476 HTML Brotli bytes until needed.

LOC: runtime/build +243 net; tests +236; dependency metadata +48; docs
+28. The separate 6,074-line vendor patch is mostly deletions of
duplicated generated ProseMirror, not 6,074 lines of application code.
No review stories, fixtures, preview providers, SDK or wire contracts
were added.

## How you verified

- Turbo app build, typecheck and lint pass; frozen dependency install
and bundle checker pass. 644 focused tests pass across targeted runs, 3
skipped.
- New real cold-HTML test retains readable text while holding the actual
implementation import and checks sanitization on release. It fails
against the prior eager implementation. Existing HTML/media/directive
tests keep their assertions, warming the implementation before testing
rendered semantics.
- New download-coordination tests catch duplicate first-use fetching;
removing suppression fails the regression test. A real Vite build test
checks emitted hashed prefetch URLs and static dependencies.
Shared-chunk bundle measurement has a regression test.
- Negative production build: eager imports of the three implementations
fail their module guards; the HTML packages also fail their demand
gates. Restored build passes.
- Production Chromium: HTML/parser fetched as Fetch at idle with no V8
coverage before use; first HTML rendering executes both from disk cache
with zero extra transfer bytes. Held/aborted requests preserve text and
composer, show skeleton/red retry line, strip hostile handlers/scripts,
and recover after reload. Cold first use now makes only the two script
requests, without extra idle fetches. Desktop and phone screenshots are
in the BB handoff.
- Matched control:
https://ymichael--6134.getbb.app/threads/thr_8bqg5t848s ; branch:
https://ymichael--6135.getbb.app/threads/thr_8bqg5t848s . Both use the
same isolated backend and read-only HTML response fixture. No user data
was changed. Chromium only; no Safari or desktop-app claim.

> AGENT GENERATED
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.

1 participant