Repository navigation
fix(app-shell,console): a plain page route stops loading the Studio designers, InterfaceListPage and one chunk per eager icon (objectui#12187) - #12226
Merged
objectstack-fleet[bot] merged 3 commits intoOct 12, 2026
Conversation
…esigners, InterfaceListPage and one chunk per eager icon (objectui#12187) - The package entry hands its designers loader to the registries (deferBuiltinDesigners); the first read of a designer registry runs it, instead of the entry's module scope on every page load. - PageView loads InterfaceListPage through React.lazy; only an interface page renders it. - The console groups the icons that a static chain from the entry reaches ($initial) into one vendor-icons-shell chunk; three measured lazy-only $initial icons stay in their own chunks. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
…n, so the registry module stays off the eager path (objectui#12187) Holding deferBuiltinDesigners in preview-registry.ts put that whole module in the console's eager closure, because the entry calls it at module scope. The loader state now sits in builtin-designers-loader.ts, which both the entry and the registries import. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
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
|
objectstack-fleet
Bot
deleted the
claude/issue-12187-page-route-chunks
branch
October 12, 2026 10:58
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.
Part of #12187
Clause-②: no
Item 2 of objectui#12187, under claim
6119197321as amended for item 2 by6121401939: what a plain page route pulls before its content. Item 1 landed as54159d3. Item 3 (onesys_user_preferenceread per load) stays on the card, so this PR leaves the card open.The measurement (H0), before and after
Instrument. The console's own
vite build(itsvite.config.ts, unchanged, plus one scratch plugin that is not committed and dumps rolldown's chunk graph: each chunk'simports,dynamicImportsand modules, and the module graph). Then the builtdist, served under/_console/with an injected base href, in a cold Chromium context (/opt/pw-browsers/chromium) on/_console/apps/demo_app/page/welcome. The backend is a Playwright route mock: one app whose navigation opens one home-kind page (apage:headerand atextblock), signed in as a member withoutmanage_metadata. "Before content" means resource entries whosestartTimeprecedes the moment the page's text is in the DOM. The resource-timing buffer is raised first: Chromium keeps 250 entries by default, and a full buffer reads as exactly 250, which is the figure the card's staging reading shows.Before = this branch's base
54159d3. After = this PR's head. Three cold runs each; the counts were identical across runs.vendor-icon-*chunks before contentvendor-icons-shellchunk)register-builtin-designers,HookDefaultInspector,ReportDefaultInspector,default-inspector-registry,InterfaceListPage*Inspector*-named chunks before contentViewVariantInspector,MetadataInspectormainin the DOM, msThe 102 requests after: 72 JS chunks, 27 API calls, 2 stylesheets, 1 favicon. The 27 API calls include the three
sys_user_preferencereads that item 3 is about.JS chunks before content, after (hash suffixes dropped):
ActionResultDialog,app-shell-eager-leaves,appRoute,AssignedUsersSection,assistantBus,builtin-designers-loader,componentRegistry,createDerive,CreateViewDialog,data-adapter,DeclaredActionsBar,dist,drillUrlFilters,ExpressionProvider,expressionUser,FlowPaletteRecentsProvider,FlowRunner,framework,i18n,i18n-locale-en,i18n-runtime,index,infrastructure,mergeServerFields,MetadataInspector,nav-selection,ObjectView,packagedActions,pageKindNode,PageView,plugin-chatbot,plugin-form,plugin-grid,plugin-view,plugins-views,preferLocal,PreviewModeContext,RecordDetailView,rolldown-runtime,runtime-metadata-persistence,RuntimeDraftBar,SchemaForm,SettingsField,SkeletonDetail,src,studioEntry,submitRedirect,ui-components,ui-layout,useActionModal,useCanAuthorMetadata,useConsoleActionRuntime,useDatasetCatalog,useMetadata,useMetaItemRefusal,useNavTargetLabel,userFilterUrlState,useSettingsLabel,useTenancyPosture,utils,vendor-dndkit,vendor-i18n,vendor-icons-core,vendor-icons-shell,vendor-markdown,vendor-objectstack,vendor-radix,vendor-react,vendor-react-markdown,vendor-ui-utils,viewFilterFold,ViewVariantInspector.Before, the same route loaded 279 single-icon chunks and these 76 others: the list above without
vendor-icons-shell,builtin-designers-loaderanduseConsoleActionRuntime, plusregister-builtin-designers,HookDefaultInspector,ReportDefaultInspector,default-inspector-registry,InterfaceListPage,identityandapiBase. (apiBaseanduseConsoleActionRuntimeare the same shared helpers, written to a chunk of a different name.)The edges, each with the module that made it
packages/app-shell/src/index.tsranimport('./views/metadata-admin/register-builtin-designers.js').then(...)at its own module scope (objectui#11939 step 2). In the graph it is adynamicImportsedge of the eagersrcchunk, so the eager-closure gate never weighs it. The browser still fetched it at boot:register-builtin-designersand 62 chunks it imports statically (HookDefaultInspector,ReportDefaultInspector,default-inspector-registry,InterfaceListPage,pageKindNode,identityand 56 single-icon chunks). Nothing on a page route reads a designer.deferBuiltinDesignersin a new module-internal module,views/metadata-admin/builtin-designers-loader.ts. The three designer registries run it once, on their first read:createObservableTypeRegistry'sgetandtypes, and the reader hooks' subscribe. Importing the entry still registers every built-in designer with no call from the host (Ruling A on objectui#11798):entryRegistersBuiltinDesigners-11939passes unchanged. A module imported without the entry (a test of one surface) keeps empty registries, as before.preview-registry.ts, and that put the whole registry module in the eager closure. In the after build the eager closure gains exactly one module,builtin-designers-loader.ts, in a 412-byte chunk.InterfaceListPage: a static import inPageView.packages/app-shell/src/views/PageView.tsximportedInterfaceListPagestatically. Its chunk is shared with the Studio'sPagePreviewand withObjectDataPage, so it is a chunk of its own, and that import fetched it on every page route.React.lazyedge, rendered underSuspensewith a spinner, in theinterfaceConfig.sourcebranch only.vendor-icon-*chunks, each imported statically by an eager chunk. The other 56 single-icon chunks on the route came with edge 1.apps/console/vite.config.ts, avendor-icons-shellgroup withtags: ['$initial'], listed aftervendor-icons-coreand before the per-icon group, at the same priority.vendor-icon-*chunk. Lazy icons keep their own chunks (1592 of them in the after build), so this is not the all-icon aggregate objectui#9251 refused. The lucide runtime stays invendor-icons-core(12 modules on both builds).$initialover-captures, measured: rolldown decides it before tree-shaking. Without an exclusion the group took 226 icons. The 3 extra arecircle-arrow-up,folder-treeandwebhook, imported only byMarketplacePackagePageand itsPluginDisclosure. The package barrel re-exports that page andviteDeclaredLazyViewsmarks it side-effect-free, so the bundle keeps it off the eager path. Those three are excluded by name (LAZY_ONLY_INITIAL_ICONS), and their chunks are lazy in the after build. The config's note says that nothing re-derives that list.Eager closure, from
pnpm check:eager-closureui-componentsThe module set grows by the one loader module and loses none. The bytes that left are gzip-stream overhead (223 streams, now one) and the shorter import lists of the chunks that import the icons. The budget line is untouched.
Not changed here: the two
*Inspector*chunks still on the routeViewVariantInspector-*.jsis in the eager closure.views/ViewConfigPanel.tsxis the runtime ObjectView's view-editor rail, in the eagerObjectViewchunk. It importsviews/metadata-admin/inspectors/ViewVariantInspector.tsxstatically. Measured with a temporaryReact.lazyon that edge (built, then restored to HEAD,git diff HEADempty): the closure loses 13,799 B gzipped more, and no*Inspector*chunk butMetadataInspectoris eager. Headroom would then be about 91.7 KB, over the 89 KB the sensitivity check allows. The check would refuse until the ceiling is re-pinned, and that is the budget line this amendment rules out. That is a decision for the seat.MetadataInspector-*.jsisviews/MetadataInspector.tsx, the runtime debug panel (MetadataPanel,useMetadataInspector) thatObjectView,PageViewandRecordDetailViewmount. It is not Studio code. It matches the acceptance's*Inspector*glob by its name only.Studio and interface pages, in Chromium on the after build
/_console/apps/demo_app/metadata/page/welcome(the page's Studio editor):register-builtin-designersand the inspector chunks load on that route. The page preview draws "Welcome / Hello from the welcome page." and the Properties form renders. The visible text is the same as on the before build. No page errors.type: 'list',interfaceConfig.source):InterfaceListPageloads and draws its own state ("Object Not Found", because the mock serves no object). The plain page loads neither chunk.The pin
packages/app-shell/src/__tests__/pageRouteLoadsNoStudioChunks-12187.test.tsxreplacesregister-builtin-designersandInterfaceListPagewith factories that count their own evaluation, and imports the package entry. It asserts:PageViewor a loader run at module scope again goes red;Ablations through objectstack's
scripts/ablation-replace.mjs. Each anchor went 1 to 0, was restored to the HEAD blob, and leftgit diff HEADempty. The predicted direction was red both times, and both went red:Tests 1 failed | 1 passed (2), "the entry loaded the designers at its own module scope: expected 1 to be +0";PageView's staticimport { InterfaceListPage }restored:Tests 1 failed | 1 passed (2), "a module in the static graph of the entry (PageView) imports InterfaceListPage statically: expected 1 to be +0".The test imports app-shell source by relative path, so no
distis in its resolution path.Co-located test edits
page-kind-writing-end-9718: the interface-mode case waits for the lazy mount (Testing Library'swaitFor). The assertion is unchanged.PageView.refreshInPlace-10519: imports../InterfaceListPageat module scope, so the module loads in the import phase and the interface case'swaitForcovers only the lazy edge (AGENTS.md 测试纪律).Tests and gates, at HEAD
0943b54entryRegistersBuiltinDesigners-11939,builtinDesigners.lateArrival-11939, the fourdesignerRegistryMissing,designerRegistryPartial,designerRegistryPopulatedand threelateRegistration-11939files: 12 files, 25 tests passed;preview-registry,inspector-registry,default-inspector-registry,InterfaceListPageorPageView:Test Files 120 passed (120),Tests 1001 passed (1001);@object-ui/app-shell:Test Files 19 passed (19),Tests 162 passed (162).type-check:@object-ui/app-shell(tsc --noEmit && tsc -p tsconfig.test.json, whoseincludeissrc/**/*.test.tsx, so it compiles the pin and the two edited tests) and@object-ui/console: both exit 0.turbo run build --filter='@object-ui/console^...' --concurrency=2: 34/34 tasks. Consolevite build: exit 0, with every build guard in the config green (assert-lazy-linter-stays-lazy,assert-types-zod-stays-lazy, declared-lazy views, ineffective-dynamic-import ledger).@object-ui/app-shellrebuilt at HEAD: its publisheddist/index.d.tsis byte-identical to the base build's (cmpexit 0), so no exported type moved.check:eager-closure("3126.7 KB gzipped across 70 of 2253 chunks", membership exact),check:eager-locale-catalogues,check:docs-route-closure,check:unreferenced-sources,check:side-effects-array,check:sdui-registration-pins,check:vi-mock-inherit,check:entry-guard,check:icon-record-names,check-changeset-presence,check:changeset-claims,check:pending-changeset-literals,check:control-bytes,check:new-line-citations("0 new citation(s)").eslint --no-inline-config --format jsonover the 8 changed.ts/.tsxfiles gave 8 files, 0 errors and 27 warnings. Every warning is ano-explicit-anythat was already there: the per-file counts equal the base's. The config has no type-aware linting (a grep forprojectService,typeChecked,tsconfigRootDirorparserOptionsexits 1; thelanguageOptionscontrol exits 0). Noeslint-rules/*.jsreads the filesystem (exit 1, 23 rule files). So the diff cannot move another file's verdict. Repo-wide lint: NOT MEASURED, CI-owned.Changeset
.changeset/12187-page-route-chunks.md:@object-ui/app-shellpatchand@object-ui/consolepatch(the console's publisheddistis chunked differently). No published export, prop or type changes.Acceptance notes
ViewVariantInspectorandMetadataInspectorwill still match*Inspector*(see above). The local request count before content is 102, and 27 of those are API calls, so the staging figure depends on the backend's own calls and on item 3.getMetadata*/listMetadata*sees the built-ins from a read after the one that started the load. In this repository, every reader outside tests uses the hooks, apart fromResourceEditPage's save handlers, which run after its own hooks have read./api/v1/runtime/config(fromindex.html'sapplyEarlyBrandingand from app-shell'sinitRuntimeConfig) and/api/v1/auth/me/localization. objectui#5544's dedup covers an in-flight request, and this mock answers at once, so this is not a reading of staging. Not classed and not filed.Generated by Claude Code