Uh oh!
There was an error while loading. Please reload this page.
perf(console): free the route views the app-shell barrel held in the eager closure, and pin the agreement - #6682
Merged
Conversation
…eager closure Six of the eight views `AppContent` declares with `lazy()` were in the console's eager closure anyway, so the browser fetched and parsed them before first render whatever the route. Re-measured on today's ref, the count of six holds and its mechanism holds for only three of them: - `DashboardView`, `PageView`, `SearchResultsPage` were eager ONLY because `packages/app-shell/src/index.ts` re-exports them and the console's entry imports that barrel statically. Tree-shaking cannot drop those re-exports because `@object-ui/app-shell` publishes no `sideEffects` field. - `RecordDetailView` is eager for a real reason: `views/ObjectView.tsx` imports it by name and `ObjectView` is in AppContent's always-needed block. - `RecordFormPage` and `ReportView` are eager through CHUNK CO-TENANCY -- rolldown emits each in a chunk it shares with a module that is eagerly used (`providers/expressionUser.ts`, `views/RuntimeDraftBar.tsx`). No import spelling repairs those; they are pinned with the co-tenant named. `scripts/vite-declared-lazy-views.ts` does both halves from one parsed list, so they cannot drift: it declares the pure route views `moduleSideEffects: false` for the console build only, and then fails the build when a declared-lazy view is eager and unpinned, or when a pinned one has quietly gone lazy. Deliberately NOT done: adding `"sideEffects"` to `packages/app-shell/package.json`. Measured on this branch, `"sideEffects": false` there moves far more but silently drops three real SDUI widget registrations (`mcp:connect-agent`, `cloud:onboarding-next`, `cloud:ai-model-status`), and an incomplete array would do the same to third-party embedders with nothing to catch it. That is a published-contract decision, not this change. Measured by `pnpm check:eager-closure` from a console `vite build`: 3237.0 KB -> 3231.7 KB gzipped (-5,367 bytes; 52 -> 49 eager chunks of 508).
os-sales
marked this pull request as ready for review
August 28, 2026 14:10
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
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 freeto 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.
Fixes#6535
Re-measured on today's ref, per view
The card was filed on an earlier ref. Re-measured on
ece68882fromapps/console/dist/eager-closure.json(files[]IS the eager set) and cross-checked by an independent BFS over the emitted chunks' static imports -- the two walks agree and my BFS reproduces the report's own chunk count exactly (52).The count of six holds. Its mechanism holds for only three of the six.
lazy()DashboardViewPageViewSearchResultsPageRecordDetailViewviews/ObjectView.tsximports it by name, andObjectViewis in AppContent's own always-needed blockRecordFormPageproviders/expressionUser.tsReportViewviews/RuntimeDraftBar.tsxComponentNavViewObjectDataPagePositive control for the two "no" rows: both views' chunks demonstrably exist on disk (
ComponentNavView-Do6FuCnd.js,ObjectDataPage-BwGRgsh1.js) and the identical query returns EAGER for six others, so the zero is a measurement rather than a mis-aimed probe. All eight get their own chunk, so absence fromfiles[]is a real lazy and not a merge artefact.The barrel correspondence is exact and it is the natural experiment: the six views the barrel re-exports are precisely the six that were eager, and the two it does not are precisely the two that were lazy.
The measured closure, quoted from
check:eager-closure's own outputBEFORE (pristine
origin/main, forced consolevite build):AFTER (this branch, HEAD
cc495a53):Delta: -5,367 bytes gzipped, 52 to 49 eager chunks. Headroom 29.6 KB to 34.9 KB. The three chunks that left, and nothing else moved:
Read plainly: this is a real, measured move, and it is small. The three views the barrel alone held eager are the three SMALLEST of the six. The 53.8 KB gzipped still on the first-paint path sits behind
RecordDetailView(47.4 KB),ReportView(4.1 KB) andRecordFormPage(2.3 KB), and no import spelling reaches any of them. That correction to the card's cost model is the more valuable half of this PR.What changed
scripts/vite-declared-lazy-views.tsdoes both halves from ONE parsed list of AppContent'slazy()declarations, so they cannot drift:moduleSideEffects: falsefor the console build only. Without that, the barrel's named re-exports are unshakeable, because@object-ui/app-shellpublishes nosideEffectsfield and every bundler must therefore assume every module in the re-export chain might do something on import.enforce: 'pre'is load-bearing and is commented as such: vite runs core plugins before normal-order ones andresolveIdis first-wins, so at normal order the hook never runs at all and the declaration is silently inert. That is not hypothetical -- it happened on this branch, and it reads exactly like "the fix does not work" rather than "the hook never ran".Deliberately NOT done:
"sideEffects"on@object-ui/app-shellThat is the general fix and it moves far more -- measured on this branch,
"sideEffects": falseon the package takes the closure to 2994.4 KB (headroom 272.2 KB). It is not shippable, and the reason is measured rather than argued: it silently drops three real SDUI widget registrations from the bundle.sideEffects: falsemcp:connect-agentcloud:onboarding-nextcloud:ai-model-statusmarketplace:installed-listrecord:attachmentsrecord:approvalsmetadata:directoryAn incomplete
sideEffectsARRAY would do the same to third-party embedders with nothing to catch it. Declaring the package's published build contract is a maintainer decision, not this card's repair -- raised as an open question in the report. The table's right-hand column is also this PR's own control: the narrow change does not reproduce the hazard.The pin, and what makes it bite
The ledger
DECLARED_LAZY_VIEWS_STILL_EAGERfails the build in BOTH directions, because the dangerous reading here is ZERO, not many:Two counter-probes guard the walk itself: every declared view must be found in SOME chunk (a matcher that matches nothing cannot fail), and
views/ObjectView.tsx-- eager by construction -- must be found EAGER. A third guard refuses to declare a view side-effect-free if its source carries a bare side-effect import, so the complement rule cannot silently turn a future view's registration into a dropped one.Ablation proving it bites, on this exact tree. Neutralising the
moduleSideEffectsdeclaration (one injected early-return; mutation confirmed on disk by blob hash9a6d2c10vs HEAD69de27ca, restored and re-verified byte-identical afterwards) makes the build fail with exit 1:Those chunk hashes are byte-identical to the pristine
origin/mainbuild, so the ablated tree reproduces the baseline exactly -- the guard catches precisely the three views this PR frees, and nothing else.Gate verdicts, each exit code captured BEFORE any pipe
All run on HEAD
cc495a53after the final commit.check:eager-closureConsole eager closure is 3231.7 KB gzipped across 49 of 508 chunks (budget: 3266.6 KB, headroom: 34.9 KB).vite build[declared-lazy-views] 5/8 views AppContent declares lazy are genuinely lazy; 3 eager, all pinned (objectui#6535).check:changeset-presenceNo source of a released package changed in this range, so no changeset is owed.(3 files changed, 0 published source)check:control-bytescheck-control-bytes: OK (scanned 5517 tracked text file(s); skipped 85 binary).check:entry-guard50 scripts/ file(s) -- no entry guard outside the baseline; 45 export bindings, 45 of them inert on importcheck:vi-mock-specifiersOK (3890 tracked source file(s), 2186 test-named; ...)check:esm-specifiersno un-ledgered package emits an extensionless relative specifier.check:self-importNo package names itself inside its own src/.check:phantom-depsEvery in-scope import is declared by the package that publishes it.check:docs-route-closure@object-ui/consoletype-checktsc --noEmit && tsc -b tsconfig.node.json --force(itstsconfig.node.jsonincludes../../scripts/vite-*.ts, so the new plugin IS type-checked)scripts/__tests__+ console-starter alias closureTest Files 85 passed (85) / Tests 2386 passed (2386)Test Files 1 passed (1) / Tests 18 passed (18)No changeset: the gate's own verdict line above is the authority -- nothing under a released package's published source changed.
Declared narrowing -- eslint
Repo-wide
turbo run lintwas NOT run; eslint was run on the three changed files only. The three things that make that a measurement rather than a gap:--format json: 3 files linted,errorCount0 andwarningCount0 on each.eslint.config.jsconfigures noprojectService, noproject, and no type-checked ruleset, so no rule's verdict on a file this PR does not touch can depend on this diff.CI runs the full farm regardless.
Findings filed, not fixed here
RecordFormPageandReportVieware held eager by a co-tenant module in their own chunk, a mechanism NO source-level search can see (git grepfinds no remaining static importer ofRecordFormPage, and it is eager anyway).providers/expressionUser.tsalso sits in an area held by finding(app-shell):buildExpressionUser's parameter is stillunknown, so the input contract objectui#6551 declared is enforced at no call site #6559, so it was not touched.lazy()declarations is eager too:metadata-adminalone is 181 KB, which is 6x today's headroom and ~34x what this PR moved. Same defect class, an order of magnitude larger, deliberately outside this card's scope.Fences respected
packages/app-shell/src/providers/**(#6559),packages/plugin-grid(#6670),packages/typesandpackages/components/src/renderers/complex/data-table.tsx(#6673),packages/sdui-parserandpackages/plugin-list(#6598),packages/i18n(#6610),examples/schema-catalog(#3965) -- none touched. The diff is 3 files: one new plugin, one new test, and the console vite config.packages/app-shell/src/index.tsis NOT modified -- the barrel's exports remain the package's public API for third-party embedders, exactly as triage required.Generated by Claude Code