Remove re-render amplifiers across slots, contexts, banners and the prompt area (C6-C13, D2, D4, E9, E11, E12, H8) - #1898
Merged
SawyerHood merged 15 commits intoAug 19, 2026
Conversation
SawyerHood
force-pushed
the
bb/mobile-perf/rerender-amplifiers
branch
2 times, most recently
from
August 19, 2026 07:50
55319c2 to
ff0b31b
Compare
SawyerHood
marked this pull request as ready for review
August 19, 2026 08:04
SawyerHood
force-pushed
the
bb/mobile-perf/rerender-amplifiers
branch
from
August 19, 2026 15:59
ff0b31b to
2e9c195
Compare
Root cause: `buildSnapshot()` allocated fresh arrays for all 14 slot kinds on every plugin registration, and the reconcile loop emitted once per plugin bundle as it resolved. Each emit gave the timeline a new `messageDirectives` array (new directive registry, every mounted markdown body re-parsed and directive cards remounted), a new `messageActions` array (static context invalidated for every row) and re-rendered all `usePluginSlots()` sites; with ~18 plugins that is ~18 app-wide passes during boot on a phone. Change: flatten each plugin's registrations once per `set` call so slot objects keep identity, rebuild kinds with structural sharing (a kind whose slot sequence is unchanged keeps its previous array; an unchanged store keeps the previous snapshot and does not notify), and hold notifications in a batch for the duration of a reconcile run with a bounded 150 ms flush window so a slow bundle cannot keep the others' UI off screen. Reads inside a batch stay consistent (lazy rebuild). Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: under `<BrowserRouter>` react-router's `useNavigate()` reads `useLocation()` and rebuilds its function on every pathname change. The thread-actions context value, the fork handler fed into the timeline static context, and every sidebar `ThreadRow`/`EnvironmentThreadGroupRow` (via `useThreadRowSplitDrag`, `useCreateThreadInWorktree` and the environment archive action) depended on it, so each navigation re-rendered every mounted row (`memo` cannot skip a context subscription) and every thread-detail refetch rebuilt `onForkMessage`, re-rendering all mounted message rows. Change: `RouteNavigationProvider` (already at the app root) now holds the live `navigate` in a ref and exposes one stable `useRouteNavigate()` for absolute app routes; the provider, the row hooks and the plugin sidebar hook use it instead of `useNavigate`. `useForkThreadFromMessage` reads the source thread from a ref so its handler identity holds across refetches. Tests assert the context value and a memoized consumer stay put across a real MemoryRouter navigation and that the fork handler keeps identity while reading the latest thread. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: `MessageActionBar` always mounted the desktop tooltip bar (one Radix Tooltip tree per action, hidden by CSS on compact coarse pointers) and then added the mobile overflow popover on top, so every message on a phone carried five-plus dead tooltip trees. Change: branch on the already computed `useMobileOverflowPopover`: on touch phones render the overflow popover or plain inline buttons (same classes, no tooltips); the desktop branch is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: every `ConversationRow` read the latest-actionable assistant/user message-id contexts, which change on each new message, so all mounted rows re-rendered their full body per message. React Compiler also refused to memoize `ConversationRow`: the inline-editor host passes `inlineMessageEditor.onHostElementChange` as a `ref`, so the compiler typed the whole object as a ref and flagged the `messageId` read during render. Change: `ConversationRow` is now a thin context reader that resolves `mobileActionDisplay` and renders a memoized `ConversationRowContent`; the ref-bearing host moved into a tiny `InlineMessageEditorHost`, so both row components now compile. A test appends a message to a 12-row timeline and asserts only the two rows whose display flips re-render. Co-Authored-By: Claude <noreply@anthropic.com>
…te provider Root cause: AppLayout and ProjectList each rebuilt the section/project name maps and the thread map on every sidebar refetch (new payload identity per turn boundary) and each provided its own `ThreadTitleMentionResources` context, so every ThreadRow, mention pill and markdown link re-rendered twice per refetch even when no title, project name or section name changed. Change: one `buildThreadTitleMentionResources(navigation, previous)` with value retention (previous maps and per-thread entries are reused when equal; the entry type narrows to the four fields mentions read), exposed through `useSidebarThreadTitleMentionResources` in AppLayout; ProjectList reads the provided context for its alpha comparator instead of building and providing a second copy. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: `usePaneContentSplitIndicator`/`useThreadGroupSplitIndicator` subscribed every mounted sidebar row to `splitLayoutAtom` and only returned `NO_INDICATOR` inside `useMemo`, which does not stop jotai from re-rendering the row when the atom changes. Splits are disabled on compact viewports but the layout still reconciles per thread navigation, so phones paid a second full-row render pass per navigation for nothing. Change: subscribe to a module-level null atom when the indicator cannot show (compact, or disabled); desktop behavior is unchanged. A test sets the layout while compact and asserts the hook does not re-render. Co-Authored-By: Claude <noreply@anthropic.com>
…ender Root cause: `usePromptDraftInputThreadIds` computed its snapshot by calling `localStorage.getItem` for every sidebar thread on every render of the subscribing component and on every keystroke notification, and it notified the sidebar (a full re-render) even when the edited draft's presence did not flip. Change: a per-subscription-set presence store caches the joined bit string, re-reads only the key that changed on notification, stays silent when that key's bit is unchanged, and drops its cache on subscribe so a flip between render and subscription is not missed. localStorage stays the source of truth (the in-memory draft cache still self-heals against same-tab external writes), so semantics are unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
…tion Root cause: `useQueries` without `combine` returns a new results array every render, so `useChildThreadPendingAttention` rebuilt its map and returned a fresh array per ThreadDetailView render (also a fresh `[]` when nothing was pending). ThreadDetailPromptArea memoizes the prompt stack on that value, so every view render invalidated `promptStack` and re-rendered the memoized `FollowUpPromptBox`. Change: module-level `combine` (TanStack structurally shares the result only for a stable combine reference) mapping to the per-child data lists, and a frozen shared empty array for the no-attention case. Hook tests assert identity across re-renders. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: babel-plugin-react-compiler bailed out of the ~1600-line
`ThreadDetailPromptArea` body, so none of its ~450 memoizable values were
cached and every draft keystroke re-ran the whole thing. Four separate
causes: `try`/`finally` blocks in the modifier-submit handler ("Handle
TryStatement with a finalizer"), three render-time ref writes
(`ref.current = value`), and composer-host closures whose ternary
returned `ref.current.draft` in one branch and the render-time draft in
the other, which made the compiler type the whole edit object as a ref
value and flag its reads during render.
Change: the sending flag flips inside a module-level
`runWhileFollowUpShortcutSending`; latest-value refs come from a tiny
`useLatestRef` hook (the render-time write lives there); the inline
attachment-error clearer is assigned in a layout effect; and the
queued/sent-message composer-host accessors are module-level functions
that take the ref. Verified with babel-plugin-react-compiler@1.0.0:
`ThreadDetailPromptArea` now reports CompileSuccess (452 slots).
Co-Authored-By: Claude <noreply@anthropic.com>
Prettier pass over the files touched so far, the navigation test drives the router from a click instead of assigning a module variable during render (react-hooks/immutability), and the two intentional render-time ref accesses (`useLatestRef`, the sidebar mention-resource cache) carry scoped eslint disables with the reason. Co-Authored-By: Claude <noreply@anthropic.com>
…icker Root cause: the workflow, todo and background-activity cards mounted their full bodies while collapsed (agent tree, todo list, one row per background task each with its own `setInterval` + state update), so a thread with several background agents paid layout for hidden DOM and ran N timers ticking out of phase, each a separate render. Change: the banner's `AnimatedBody` (realize on first expand, retain afterwards) moves to its own module and the three cards use it, keeping their existing collapsed border look via `collapsedBorder`. Live durations read one shared `useSecondTick()` store (a single interval that starts with the first subscriber and stops with the last). Tests cover realize/retain and that a collapsed card runs no timer while an expanded three-row card runs one. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: `experimental_useSidebarThreads` remapped every `ThreadListEntry` into a fresh `PluginSidebarThread` on each sidebar update, so a memoized plugin row still re-rendered for every other thread's change even though React Query keeps unchanged entries' identity. Change: a WeakMap memo keyed on the entry (and the host-name map instance it was built with) returns the same DTO while the entry is unchanged. The SDK contract and the audit entry now state that the array is uncapped and rows are expected to be windowed; the bundled declarations are regenerated for the comment. Test asserts DTO identity for the untouched thread across an update. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: `useIsSidebarShowing`/`useOptionalIsSidebarShowing` read the whole `SidebarContext` value, which changes on every provider commit (a mobile close is two: the closing flag, then the deferred state flip), so the page header and the retained ~1000-line `ThreadSecondaryPanel` body re-rendered on each even though they only need one boolean. Change: the provider publishes `isSidebarShowing` through a dedicated boolean context; the two hooks read it. A test flips a suppress flag and closes the mobile drawer and asserts the reader renders only when the bit flips. The full context split (actions vs. state) stays a follow-up. Co-Authored-By: Claude <noreply@anthropic.com>
Render counting via array pushes instead of reassigning a module variable (react-hooks/globals), a fixed `startedAt` in the banner test (react-hooks/purity), and one scoped disable block around the deliberate render-time cache in `useSidebarThreadTitleMentionResources`. Co-Authored-By: Claude <noreply@anthropic.com>
…tion in Ladle - plugin-sidebar-hooks: derive the host-name map per hosts payload at module level so two useSidebarThreads callers keep the same WeakMap DTO entries instead of evicting each other's; test covers two consumers. - plugin-slots: batch closer never drives the depth below zero after a test reset. - Ladle global provider mounts RouteNavigationProvider so sidebar/thread stories that now navigate through useRouteNavigate do not throw on click. Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood
force-pushed
the
bb/mobile-perf/rerender-amplifiers
branch
from
August 19, 2026 16:13
2e9c195 to
c78cfa9
Compare
SawyerHood
added a commit
that referenced
this pull request
Aug 19, 2026
…lots (A5, partial) (#1897) ## What was wrong Every plugin app bundle carried its own copy of libraries the host already ships (audit A5). zod (~516 KB raw / ~75 KB gzip) was bundled by four builtins, tailwind-merge + clsx by thirteen, class-variance-authority by most, and the shared-ui `Icon` with its ~110 KB hugeicons map by twelve. Phones parsed and evaluated all of it on every page load. `RUNTIME_SLOT_BY_SPECIFIER` in packages/plugin-build only shimmed react, the SDK, pierre, the portal radix families, sonner and vaul. ## What changed - `bb plugin build` routes `zod`, `clsx`, `tailwind-merge`, `class-variance-authority` and `@bb/shared-ui/icon` to new `globalThis.__bbPluginRuntime` slots (`zod`, `clsx`, `tailwindMerge`, `classVarianceAuthority`, `sharedUiIcon`). shared-ui's own components import the icon module as `./icon`; the esbuild plugin routes that import to the same slot when the importer lives in shared-ui's source tree. A plugin's vendored `icon.tsx` still bundles. - Shims forward a real default export (`mod.default` when the host namespace has one), so `import clsx from "clsx"` receives the callable, not the namespace. - Host (`apps/app/src/lib/plugin-frontend.ts`) installs the five slots. Export manifest regenerated (`@bb/shared-ui/icon` exports come from esbuild metadata like the SDK facade). - `PLUGIN_SDK_VERSION` 0.4.8 -> 0.4.9. Old bundles keep working (they carry their own copies); new bundles need the new host slots, so path/builtin plugins rebuild on the sdkVersion-differs trigger and scaffolds pin the floor. - Scaffold generator moves clsx/tailwind-merge/cva to type-only deps (zod stays a dependency: server.ts bundles it). Guide and bb-plugin-authoring skill updated. CLI scaffold-dependency test now applies the shim exemption only to app-side files. - `apps/app/bundle-budget.json` raised: exposing the whole zod namespace stops the boot chunk from tree-shaking zod exports the app never calls: boot payload 1655.7 KB / 449.1 KB brotli -> 1848.4 KB / 481.7 KB (+193 KB raw, +33 KB brotli). The other four slots add under 2 KB. Removing `zod` from RUNTIME_SLOT_BY_SPECIFIER reverts both sides of the trade. Builtin app.js after rebuild: 8.87 MB -> 4.01 MB raw (-55%), 1.76 MB -> 0.93 MB gzip (-47%). Per plugin (raw): ask-user-question 784 KB -> 38 KB, automations 1063 -> 315, connect 376 -> 145, custom-instructions 99 -> 9, docs 1640 -> 1419, github 416 -> 185, inline-vis 239 -> 9, keep-awake 308 -> 79, memory 122 -> 30, provider-retry 249 -> 18, secrets 644 -> 37, side-chat 241 -> 11, tasks 2380 -> 1638, workflows 300 -> 69. ## How you verified - New tests in `packages/plugin-build/src/build-plugin-app.test.ts`: every non-SDK slot has a manifest entry; a bundle importing the five libraries compiles with no node_modules and, executed against a fake runtime, yields the callable clsx default, `z`, `twMerge`, `cva`, `Icon`; shared-ui's relative `./icon` import is shimmed while a plugin's own `components/ui/icon.tsx` still bundles; `isSharedUiIconRelativeImport` unit cases. `apps/app/src/lib/plugin-frontend.test.ts` asserts the new slots and their identity with the host modules. - `pnpm exec turbo run typecheck lint` for @bb/plugin-build, @bb/templates, @get-bb/plugin-sdk, @bb/domain, @bb/app, @bb/cli, @bb/server: green. `pnpm exec turbo run test` for @bb/plugin-build (24 passed), @bb/templates (44), @bb/plugin-registry (1), plugin-sdk version test, CLI plugin-build/plugin-new/packaged-plugin-build/plugin-guide-docs/plugin-dev-loop/docs- and github-official-plugin-bundle/plugin-scaffold-dependencies tests, server plugin-app-bundle + plugin-sdk tests, app plugin-frontend test: all green. - `pnpm exec turbo run build --filter=@bb/app` + `node scripts/check-bundle-budget.mjs`: passes with the ratchet. - Rebuilt all 18 plugin app bundles through buildPluginApp (sizes above). Fixes: part of the mobile / iOS Safari performance program (verified sweep report in the bb thread; no single issue). ## Stack context Layer 18 of 22 in the `bb/mobile-perf/*` stack (bottom → top: quick wins first, big rocks last). - Prerequisite (layer below): `bb/mobile-perf/plugin-boot-defer` (#1896). - Next layer: `bb/mobile-perf/rerender-amplifiers` (#1898). - Audit findings addressed: see title IDs. Review: approved-with-fixes; 1 review fix commit(s). - Partial: A5 (bundle splitting): not implemented (outdir + esbuild splitting + served chunks). Slots shipped; splitting plan described in the report. - Reviewer notes / follow-ups: A5 'splitting' (outdir + splitting, lazy definePluginApp components) is not implemented; the branch only covers the host-slot half of A5, as the implementer reported. | Design trade-off to be aware of, not a defect: exposing the full zod namespace on __bbPluginRuntime stops the host boot chunk from tree-shaking zod (+193 KB raw / +33 KB br on boot | Compatibility note (pre-existing pattern, same as the earlier @pierre/diffs slot): a plugin app built with the new bb plugin build throws 'Cannot load "zod"' if loaded by an older - Wire/contract: No server<->host-daemon wire change; HOST_DAEMON_PROTOCOL_VERSION untouched. Plugin runtime contract: five new slots on globalThis.__bbPluginRuntime (zod, clsx, tailwindMerge, classVarianceAuthority, sharedUiIcon); RUNTIME_SLOT_BY_SPECIFIER gains zod, clsx, tailwind-merge, class-variance-authority, @bb/shared-ui/icon; runtime shims now export `("default" in mod ? mod.default : mod)` as default. PL > AGENT GENERATED: by Claude Code (claude-mangosteen-eap) --------- Co-authored-by: Claude <noreply@anthropic.com>
SawyerHood
added a commit
that referenced
this pull request
Aug 19, 2026
…d; row containment (C1, C3, C4) (#1899) ## What was wrong - The in-progress assistant message re-ran the whole react-markdown pipeline on every text delta (`MarkdownPreview` memoizes on `content ===`), 2-5 full parses per second of a growing document, 10-30 ms each on a phone (C3). - Mermaid blocks called `mermaid.render` on mount and on every streaming delta, with no viewport gate and no reuse of a previous SVG (C4). - Every loaded top-level timeline row stayed in layout/paint; each style/layout pass on a phone (keyboard, orientation, streaming growth) walked all mounted rows (C1 step 1). ## What changed - `streaming-markdown-split.ts`: `splitStreamingMarkdown(text)` returns a settled prefix and a live tail. The boundary is the last blank line that is outside an open fenced block / `$$` math block / list continuation and is followed by a complete line, so the boundary only moves forward as text streams. `ConversationMessageContent` renders the streaming assistant row as two memoized `MarkdownPreview` documents (settled + tail) and falls back to one document when no boundary exists or when the row is complete. Seam classes restore the `last:mb-0` / `first:mt-0` margins at the split so completion does not shift layout. `ThreadTimelineRows` computes the streaming row (`findStreamingAssistantMessageId`: trailing leaf row while the runtime runs, descending pending turns and pending delegations) and provides it through a context; delegation output uses the split while pending. - `markdown-mermaid-render-cache.ts` + `MarkdownMermaidDiagram`: one shared IntersectionObserver gates the first render (256px root margin); source changes after the first render are debounced 300 ms trailing and keep the previous SVG visible; rendered SVGs are stored in a 32-entry LRU keyed by (source, theme, palette epoch); a cached diagram paints synchronously on mount. - `timeline-row-containment.ts` + `TimelineRowsList`: top-level row wrappers get `max-md:[content-visibility:auto] max-md:[contain-intrinsic-block-size:auto_1.25rem]`; conversation rows add an inline length-based `contain-intrinsic-block-size` estimate (bucketed). Nested lists keep plain wrappers. Compact-only because paint containment clips the assistant table breakout on wide layouts. ## How you verified - New tests: `streaming-markdown-split.test.ts` (fences incl. marker matching, `$$` blocks and inline `$$x$$`, loose lists / indented continuation, headings, monotonic boundary while streaming); `ConversationMessageContent.streaming.test.tsx` (mocks react-markdown to count parses: only the tail re-parses per delta, boundary moves forward, completion collapses to one document, open fence stays in the tail); `ThreadTimelineRows.streaming.test.ts` (streaming row selection incl. pending turn children, pending delegation childRows, completed turn -> null); `markdown-mermaid-diagram.render.test.tsx` (no render before viewport entry, one shared observer, 300 ms debounce keeps previous SVG, remount served from cache without calling mermaid, LRU eviction); `ThreadTimelineRows.containment.test.tsx` (top-level wrappers get the class + conversation rows carry the estimate, nested wrappers do not). - `pnpm exec turbo run typecheck --filter=@bb/app`: pass. `pnpm exec turbo run lint --filter=@bb/app`: 0 errors, 147 warnings (same count as baseline; touched files carry the same 12 pre-existing warnings as before). `pnpm exec turbo run test --filter=@bb/app -- src/components/thread/timeline src/components/ui/markdown-* src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx src/views/thread-detail`: 48 files, 411 tests pass. ## Update (2026-08-19) `content-visibility: auto` is now armed only where CSS scroll anchoring exists (`CSS.supports("overflow-anchor", "none")`: Chromium, Firefox). WebKit has no scroll anchoring, so a replayed size that differs from the real row size moved the visible content and read as a flash-and-scroll while a thread settled on iOS. iOS keeps the inert intrinsic-size estimate; Chromium/Firefox keep the layout/paint savings. Test added for the WebKit path. Fixes: part of the mobile / iOS Safari performance program (verified sweep report in the bb thread; no single issue). ## Stack context Layer 20 of 22 in the `bb/mobile-perf/*` stack (bottom → top: quick wins first, big rocks last). - Prerequisite (layer below): `bb/mobile-perf/rerender-amplifiers` (#1898). - Next layer: `bb/mobile-perf/mobile-interactions` (#1900). - Audit findings addressed: see title IDs. Review: approved-with-fixes; 1 review fix commit(s). - Reviewer notes / follow-ups: C1 still needs an iOS Simulator pass (Linux review box; caniuse confirms iOS Safari through 26.3 has no scroll anchoring). With the fix, rows are laid out once before opting in, so | C3 (design trade-off, not fixed): plugin message-directive components and stateful markdown widgets (code-block wrap toggle) that sit in the live tail remount when the boundary mov | C4: cached SVGs reuse the first renderer's element ids, so two simultaneously mounted copies of the same diagram (e.g. split view of the same thread) share ids; visually identical | C1: paint containment can clip the assistant table breakout by ~2px inside expanded turn bodies on compact (nested list is pl-3/pr-2 asymmetric). Negligible. - Wire/contract: None. No server/daemon wire, route, command or protocol-version change. New required prop `streaming: boolean` on the assistant variant of ConversationMessageContent (app-internal component contract; stories/tests updated). > AGENT GENERATED: by Claude Code (claude-mangosteen-eap) --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Several client-side amplifiers made phones re-render far more than the data changed:
useNavigate()under<BrowserRouter>rebuilds per pathname, so the thread-actions context, the fork handler in the timeline static context, and every sidebar row (useThreadRowSplitDrag,useCreateThreadInWorktree, environment archive) re-rendered on every navigation.MessageActionBarmounted the hidden desktop tooltip bar on touch phones in addition to the mobile popover.ConversationRowre-rendered per new message via the latest-actionable id contexts, and React Compiler skipped it (a ref-callback passed asreftyped the edit object as a ref).splitLayoutAtomon compact viewports where splits are off.useChildThreadPendingAttentionreturned a fresh array per render, invalidating the memoized prompt stack.ThreadDetailPromptAreafailed React Compiler compilation (try/finally, render-time ref writes, ref-typed closures).experimental_useSidebarThreadsremapped every entry into a fresh DTO per update.What changed
useRouteNavigate()fromRouteNavigationProvider(ref-held navigate, stable identity, no location subscription) used by rows, ThreadActionsProvider, fork handler and plugin sidebar hooks; fork handler reads the thread from a ref.ConversationRow= thin context reader +memo(ConversationRowContent);InlineMessageEditorHostisolates the ref callback so both compile.buildThreadTitleMentionResourceswith value retention; one provider (AppLayout); ProjectList reads the context.combine+ frozen empty array for child attention.runWhileFollowUpShortcutSending,useLatestRef, layout-effect ref write, module-level composer-host accessors).AnimatedBody(realize on first expand, retain) for the three cards;useSecondTickshared ticker.SidebarShowingContextboolean foruseIsSidebarShowing/useOptionalIsSidebarShowing.How you verified
pnpm exec turbo run typecheck --filter=@bb/app --filter=@get-bb/plugin-sdk --filter=@bb/plugin-build: pass.pnpm exec turbo run lint --filter=@bb/app: 0 errors (pre-existing compiler warnings only).pnpm exec turbo run test --filter=@bb/app: 370 files, 2910 passed, 3 skipped.@get-bb/plugin-sdktests: 103 passed.@bb/plugin-buildtests: 20 passed.ThreadDetailPromptArea(452 slots),ConversationRow,ConversationRowContentnow CompileSuccess.Fixes: part of the mobile / iOS Safari performance program (verified sweep report in the bb thread; no single issue).
Stack context
Layer 19 of 22 in the
bb/mobile-perf/*stack (bottom → top: quick wins first, big rocks last).bb/mobile-perf/plugin-host-slots(Shim clsx, tailwind-merge, cva and the shared icon onto plugin host slots (A5, partial) #1897).bb/mobile-perf/markdown-streaming-and-containment(Split streaming markdown into settled prefix + live tail; gate mermaid; row containment (C1, C3, C4) #1899).