Repository navigation
perf(bundle): drop axios, split vendors per entry, unify on one virtualizer - #1856
Merged
Merged
Conversation
…alizer The desktop shell shared a single `vendors` chunk with the browser Mobile Remote entry, so it downloaded and parsed Supabase, the react-markdown stack and the Mobile Remote's icon set that only `mobileRemoteEntry.tsx` imports. Naming the initial vendor chunk by the entries a module belongs to keeps each document to its own dependencies. `axios` was imported by exactly one file, and only to register interceptors on the global instance — nothing in the app ever issued an axios request, and `installFetchTracking()` already covers real traffic. The app also carried two virtualization libraries. Every windowed list except the CodeViewer diff/code panes now runs on `@tanstack/react-virtual` through a new shared `VirtualList` primitive, which takes react-virtuoso out of the boot graph. Desktop initial JS 1,558 -> 1,024 KB; boot JS 6.88 -> 6.26 MiB.
FresnelR27
force-pushed
the
perf/bundle-vendor-split-single-virtualizer
branch
from
September 16, 2026 03:22
42d217e to
5dc399f
Compare
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.
Problem
Three separate sources of avoidable frontend weight, all measured from a
production
pnpm build:statsondevelop:The desktop shell paid for the Mobile Remote's dependencies.
main(
src/index.tsx, the Tauri desktop shell) andmobile(
src/mobileRemoteEntry.tsx, the browser Web Remote) are separate HTMLdocuments that never load each other's code, but
splitChunksput theirnode_modules into one shared
vendorschunk. BecausemobileRemoteEntry.tsxstatically imports Supabase,
react-markdown/remark-gfmand its own iconset, the desktop boot bundle downloaded and parsed all of it — 744 KB of
Supabase, the micromark stack and 123 KB of glyphs it never uses.
axiosshipped for nothing. It was imported by exactly one file,src/util/monitoring/apiTrackerHttp.ts, and only to register interceptors onthe global axios instance. Nothing in the app ever issued an axios request:
the
apiConfig.tsits comment referenced does not exist,src/api/http/uses
fetch, andinstallFetchTracking()in that same file alreadyinstruments real traffic. 49 KB of boot JS for zero behaviour.
Two virtualization libraries.
@tanstack/react-virtual(18 KB) drovechat history, Gantt, Kanban, the Spotlight pickers and the PR/issue lists,
while
react-virtuoso(58 KB) drove the file trees, diff lists, DevToolsconsole and Project Manager sidebars — with each call site re-deriving its
own scroll handling.
This also resolves the open question behind the red nightly bundle-budget gate:
the budget has been over since the
mobileentry landed, pending a decision onwhether that entry should share the desktop
vendorschunk. It should not.Solution
Per-entry vendor chunks.
initialVendorsnow names its chunk after theentries a module actually belongs to, yielding
vendors.main,vendors.mobileand
vendors.main-mobile. Genuinely shared packages (React, Jotai, i18next) arestill emitted once and shared; single-entry packages stay out of the other
document. No module is duplicated — total emitted JS is unchanged at 27.9 MB,
the bytes just stopped being loaded by the wrong page.
axiosremoved frompackage.json, the lockfile, and the interceptor halfof
apiTrackerHttp.ts. The now-dead pre-capture path it existed for(
captureApiCallStack, its pending-info map,clearPendingHttpTrackingState,the axios-XHR de-duplication guard) went with it, along with the axios-internal
frame names in the stack filter. Fetch and XHR tracking are untouched.
One virtualizer. New
src/components/VirtualListwraps@tanstack/react-virtualwith the prop surface the react-virtuoso call sitesused (
data/totalCount,itemContent,computeItemKey), plus sticky groupheaders, a footer slot and a one-shot
endReached. Two conversions are handledcentrally because they are easy to get wrong per-site: Virtuoso buffers in
pixels where TanStack buffers in rows, and Virtuoso measures every row where
TanStack measures only rows attached to
measureElement.Migrated onto it: the four shared components that fan out to ~20 surfaces
(
VirtualizedStickyTree,TreeRow/VirtualizedListBase,DiffSectionList,VirtualizedGroupedList) and seven plain lists. The custom VirtuosoScrollercomponents those shared trees used only to reach the scroll element and its
onScrollare gone —VirtualListexposes both directly.DiffSectionListnow owns its scroll snapshot type instead of persisting Virtuoso's
StateSnapshot; that state is in-memory only, so no migration is needed.react-virtuosoremains a dependency forfeatures/CodeViewer(
ModernCodeViewer,VirtualizedModernDiff), which are reached only throughimport(). Those are the chat pane's code-block and diff renderers, deliberatelyleft for a follow-up so this PR does not put that path at risk. A
startupGraph.test.tsentry now pins react-virtuoso out of the boot graph.Measured result:
check:bundle-budgetJSconfig/bundle-budget.jsonmaxJsBytesis ratcheted 6,700,000 → 6,600,000 perthat file's own rule.
Potential risks
The file tree, Source Control, Search results, the diff list and the DevTools
console all depend on scroll-position preservation across tab switches, tree
expansion and remounts. That logic was reimplemented on
VirtualList(anchor-by-path restore, mount-time offset restore, reset-to-zero recovery)
and is exercised by the existing suites, but it has not been driven in the
running app. Reviewers should open My Station, scroll a large file tree,
switch tabs and come back, and expand/collapse folders.
VirtualizedGroupedListare now rendered byVirtualList'sstickyIndices+rangeExtractorrather than byGroupedVirtuoso. Covered by tests, but visually unverified.VirtualizedStickyTreelost itsrangeChangedcallback. It wasdocumented as a backup anchor update; the throttled scroll handler is the
primary one and covers every viewport move. If an anchor is ever missed after
a non-scroll viewport change, this is the place to look.
VirtualizedStickyTree'svirtuosoRefprop isnow
listRef: RefObject<VirtualListHandle>, andVirtualizedListBaseHandle.getVirtuosoRef()is removed — it had no callersand returned a type from the library being dropped.
overscansemantics.VirtualizedListBasedocumentedoverscanas arow count while Virtuoso read it as pixels; it is now honoured as rows
(
overscan * itemHeightpx), so those lists render a larger buffer thanbefore. No consumer overrides the default.
@tanstack/react-virtualwas already a directdependency;
axiosand its transitive tree (77 lockfile lines) were removed.Rollback is a revert — no data, schema or wire format is touched.
useChatGroupsProjectiontests ondevelop; they come from in-flightempty-turn-grouping work in the shared checkout, not from this change.
Verification
Ran:
pnpm build:stats+pnpm check:bundle-budget— boot JS 6.26 MB against theratcheted 6.29 MB budget, passes (it was over before this change).
npx tsc --noEmit— clean, 0 errors. NeedsNODE_OPTIONS=--max-old-space-size=8192; it OOMs at the default heap.pnpm lint:fast(oxlint) — clean.npx eslintover every changed directory with--max-warnings 0— clean.This also fixed a pre-existing
react-hooks/refsviolation inVirtualizedListBase(refs written during render) by moving those writesinto a layout effect.
npx vitest run src/components src/modules src/engines/ChatPanel src/features/CodeViewer src/scaffold— 1,101 files / 6,657 tests pass.The only 2 failures are the pre-existing
useChatGroupsProjectioncasesdescribed above.
src/app/root/__tests__/startupGraph.test.ts— passes with the newreact-virtuoso guard, confirming it is no longer statically reachable from
boot. The emitted
async-vendors.react-virtuoso.jsis now a lazy chunk.New tests:
src/components/VirtualList/index.test.ts(7 cases — windowing, thepixel→row overscan conversion, one-shot
endReached, sticky-header pinning,findStickyIndex).VirtualizedGroupedList.test.tswas rewritten offVirtuosoMockContextontooffsetHeight/ResizeObserverstubs.VirtualizedListBase.test.tswas deleted: its only subject,resolveInitialTopMostItemIndex, was a workaround for Virtuoso normalizinginitialTopMostItemIndex, and no longer exists.Did not run: the app itself. Per the risks above, the scroll-preservation
behaviour in My Station, Project Manager and the chat Changes/Timeline views
has not been exercised in the running Tauri app, and there are no screenshots.
Also not run: Playwright E2E,
cargo test(no Rust changes).