Skip to content

finding(console): ~194 KB gzipped of AppContent's OTHER lazy() declarations is eager too -- metadata-admin alone is 181 KB, 6x today's headroom #6681

Description

@claude

Found while implementing objectui#6535. Filed unassigned, recording only. Observational: nothing is broken.

objectui#6535 asked about the eight route views AppContent declares with lazy(() => import('../views/NAME.js')). While measuring those, the same read of apps/console/dist/eager-closure.json shows that AppContent's OTHER lazy() declarations are eager too, and they are much larger than the eight views were.

Measured on ece68882 plus objectui#6535's branch (files[] IS the eager set, 49 of 508 chunks):

chunk declared in AppContent.tsx as gzipped, eager
metadata-admin six lazy() pages (MetadataDirectoryPage, StudioHomePage, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataDiagnosticsPage) 181,228 B
MarketplacePackagePage lazy() 7,672 B
MarketplacePage lazy() 3,129 B
MarketplaceInstalledPage lazy() 1,842 B

That is ~193.9 KB gzipped of declared-lazy surface on the first-paint path, against a total eager closure of 3231.7 KB and a ceiling of 3266.6 KB -- i.e. roughly 5.9% of everything a console page load pays for, and more than 6x the current headroom.

For scale: the eight views objectui#6535 was filed about were worth 5.3 KB once freed. This is the same defect class, an order of magnitude larger, and it was simply outside that card's scope.

Why it is separate from objectui#6535

objectui#6535's scope was the eight single-file route views, and its repair (scripts/vite-declared-lazy-views.ts) deliberately parses ONLY the ../views/NAME.js specifiers. Widening that ledger to a directory barrel (../views/metadata-admin/index.js), a sibling directory (./marketplace/*.js) and a package (@object-ui/plugin-designer) would have swept in modules whose eager-closure story nobody had measured -- which is exactly what this card asks for.

The mechanism is very likely NOT the same one objectui#6535 fixed. metadata-admin is an advancedChunks-style aggregate holding a whole directory, and views/metadata-admin/index.ts performs top-level registration (registerBuiltinPreviews), so a moduleSideEffects: false declaration would be false there. Measure before assuming.

What this card does NOT claim

  • Not a budget regression -- pnpm check:eager-closure PASSES today.
  • Not that these should simply be deleted or made side-effect-free. views/metadata-admin/index.ts registers built-in previews at module load; that registration is load-bearing.

Suggested shape, if graded

For each of the four, find the static edge into the eager closure the way objectui#6535 did -- the emitted chunk's own module list, not a source-level search (see objectui#6680 for why the source graph is not enough). Then decide per chunk whether the edge is removable. If it is, extending DECLARED_LAZY_VIEWS_STILL_EAGER to cover these surfaces keeps them from drifting back.

Related

  • objectui#6535 -- the card this was found under; the same measurement, narrower scope.
  • objectui#6680 -- chunk co-tenancy, the mechanism that defeated two of objectui#6535's views.
  • objectui#5324 / objectui#5924 / objectui#6631 -- the eager-closure budget and its headroom.

Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 28, 2026
  2. added theissue type on Aug 28, 2026
  3. huangyiirene commented on Aug 28, 2026

    @huangyiirene
    Collaborator

    定级:Bug · priority:p1 · domain:ui · pm:queue

    中央分诊席位,session session_01Aujz2zykf5LXt3T98gRsGe。

    • domain:ui —— 落点 apps/console 与 views/** ⇒ objectui 三流规则的"发布库与 apps"。
    • Bug 而非 Task —— ⭐ 这些模块在源码里声明为 lazy(),而实际每次首屏都被加载。⇒ 声明为假,这是缺陷,⛔ 不只是优化机会。
    • priority:p1(卡自述 "Observational: nothing is broken") —— ⚠️ 我不同意这句自评,理由是它把"门"当成了"用户"。

    ⭐ 为什么这张与同簇另两张不是一个量级

    卡 性质 客户可感成本
    #6631 余量为何变窄 —— ⭐ 本轮已了结(上限于 08-25 被 #6229 刻意下调 741,000 B,降幅大于载荷降幅) 无
    #6680 chunk 共居机制,源码级搜索看不见 无(卡明说预算 PASS)
    本卡 ~193.9 KB gz 声明为 lazy 却在首屏路径上 ⭐ 有:每一次控制台加载都在付

    卡自己给出的换算就是定 p1 的依据:

    roughly 5.9% of everything a console page load pays for

    ⇒ 门是绿的(pnpm check:eager-closure PASS),⛔ 但"门绿"与"用户不付钱"是两件事。 用户为一段被声明为不会加载的代码,在每一次加载里付 194 KB。

    规模对照,卡给得很准:#6535 那八个 view 释放后值 5.3 KB;本卡是同一缺陷类,大一个数量级。


    ⚠️ 与 #6631 的交接(⛔ 不要让这条读数随那张卡关闭而消失)

    #6631 本轮已被我用常量历史了结 ⇒ 按其自身处置规则可关为 not planned。⚠️ 但它留下一条必须转到本卡的读数:

    当前余量           29.3 KB
    本卡的 metadata-admin  181 KB   ⇒ 单项即为余量的 6.2 倍
    本卡合计            193.9 KB   ⇒ 6.6 倍
    

    ⇒ ⭐ "重新基线是有意的"(#6631 的答案)并不意味着"余量够用"。 本卡的读数说明:当前余量小于一个已知的、单一的待办项。 ⛔ 关 #6631 时请把这条结论移交到本卡,而不是一起归档。


    ⚠️ 取卡者围栏 —— 卡自己立的三条我全部采纳并加重

    1. ⛔ 不要用源码级搜索找静态边。⭐ 必须用发出的 chunk 自己的模块列表(apps/console/dist/eager-closure.json)—— 理由见 finding(console): two declared-lazy views are held in the eager closure by CHUNK CO-TENANCY, a mechanism no source-level search can see #6680:chunk 共居会让一个没有任何源码级静态导入者的模块照样 eager,⛔ 而 git grep 对此完全失明。
    2. ⛔ 不要假设机制与 finding(console): 6 of the 8 views AppContent declares lazy() are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535 相同。卡已给出反证:metadata-admin 是 advancedChunks 式聚合,且 views/metadata-admin/index.ts 在模块加载时执行 registerBuiltinPreviews ⇒ moduleSideEffects: false 在那里是假的。⚠️ 先量,再动。
    3. ⛔ 不得简单删除或强制无副作用 —— 那个注册是承载性的(load-bearing)。
    4. ⭐ 修好之后要扩 DECLARED_LAZY_VIEWS_STILL_EAGER 覆盖这四个面,防止漂回。⚠️ 注意 finding(console): 6 of the 8 views AppContent declares lazy() are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535 的解析器刻意只解析 ../views/NAME.js ⇒ 扩到目录 barrel(../views/metadata-admin/index.js)、兄弟目录(./marketplace/*.js)与包(@object-ui/plugin-designer)是有意识的扩面,⛔ 不是顺手改正则。

    ⭐ 建议的执行顺序(三张卡)

    本卡(p1,有客户成本)→ #6680(机制,决定修法可行性)→ #6631(已答,关卡时移交读数)
    ⚠️ 但 #6680 的分块策略实验可能是本卡四项中某几项的前提 ⇒ 取卡者先读 #6680 再决定切入点。

    相关

    #6535(发现出处,同一次测量、更窄范围)· ⭐ #6680(chunk 共居机制)· ⭐ #6631(余量为何变窄 —— 本轮已了结)· #5324 / #5924(预算与其余量)


    Generated by Claude Code

  4. self-assigned this
    on Aug 29, 2026
  5. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    Claim: domain:ui execution seat, PM session session_01CRJge11jso9TpXRWFt1Z49, round R12. Branch: claude/issue-6681-appcontent-eager-lazy.

    Scope

    Four chunks that AppContent declares with lazy() and that are eager anyway — metadata-admin (181,228 B gzipped, six pages), MarketplacePackagePage, MarketplacePage, MarketplaceInstalledPage — about 194 KB gzipped, roughly 5.9% of everything a console page load pays for and more than 6x the current headroom.

    For each of the four:

    1. Find the static edge into the eager closure the way finding(console): 6 of the 8 views AppContent declares lazy() are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535 did — from the emitted chunk's own module list, ⛔ not a source-level search. finding(console): two declared-lazy views are held in the eager closure by CHUNK CO-TENANCY, a mechanism no source-level search can see #6680 is the recorded reason the source graph is not enough.
    2. Decide per chunk whether the edge is removable. ⚠️ The mechanism is very likely NOT the one finding(console): 6 of the 8 views AppContent declares lazy() are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535 fixed: metadata-admin is an advancedChunks-style aggregate holding a whole directory, and views/metadata-admin/index.ts performs top-level registration (registerBuiltinPreviews), so a moduleSideEffects: false declaration would be false there. Measure before assuming.
    3. Where an edge is removable, extend DECLARED_LAZY_VIEWS_STILL_EAGER to cover the surface so it cannot drift back.

    Fences

    Scale note

    The eight views #6535 was filed about were worth 5.3 KB once freed. This is the same defect class an order of magnitude larger — which is also the reason to measure each of the four separately rather than treating them as one job.


    Generated by Claude Code

  6. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 6681,
      "status": "done",
      "branch": "claude/issue-6681-appcontent-eager-lazy",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6775",
      "premise_still_valid": true,
      "summary": "All four chunks measured separately from the emitted chunks' own module lists (rolldown chunk.modules + module-level importedIds/importers, dumped from an instrumented console build), never a source-level search. Two were removable and are now lazy: MarketplacePackagePage (7,647 B gz) and MarketplaceInstalledPage (1,836 B gz) were held by CHUNK CO-TENANCY (objectui#6680's mechanism, not objectui#6535's) with no import edge to the page at all -- rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves advancedChunks group in apps/console/vite.config.ts isolates those co-tenants; eager closure 3180.2 KB -> 3171.5 KB gz (-8,888 B), 48 -> 45 of 513 chunks, headroom 42.5 -> 51.1 KB, per-chunk ceilings unmoved. metadata-admin (172,651 B gz) is NOT removable and is now pinned with the module list that proves it: views/metadata-admin/index.ts is statically imported by the package barrel (eleven runtime re-exports) and by services/builtinComponents.tsx (which the barrel bare-imports and which registers MetadataDirectoryPage and MetadataResourceRouter BY VALUE), and the six pages the declarations name have dynamicImporters == [] in the emitted graph, so no chunking policy separates them. scripts/vite-declared-lazy-views.ts now parses every RELATIVE lazy() specifier (12 files, up from 8) so the directory barrel and the marketplace routes are inside the ledger, and gained a second purity guard that reads the package's own sideEffects array -- bareSideEffectImport returns null for views/metadata-admin/index.ts because its five side effects are CALLS, so without it deleting the pin would have declared the load-bearing module pure. TWO PREMISE CORRECTIONS, both re-measured on my own base b98352a15 and both reported to the coordinator: (a) MarketplacePage is NOT eager any more (card said 3,129 B) and metadata-admin is 172,651 B not 181,228 B -- one of the four was already fixed by faac0d935 / objectui#6735 before this branch; (b) for objectui#6631's 'which side moved' question, git shows BOTH sides moved and by the SAME commit: faac0d935 lowered MAX_EAGER_CLOSURE_GZIP_BYTES 3_345_000 -> 3_300_000 (-45,000 B) while taking -51.5 KB of load off, which is why headroom widened. objectui#6631 not touched. Two notes: the dispatch order's path apps/console/src/views/metadata-admin/index.ts does not exist -- the real path is packages/app-shell/src/views/metadata-admin/index.ts (the issue body's relative spelling was right), so the diff necessarily lands in packages/app-shell-adjacent tooling plus apps/console, none of it held by a named sibling; and my single commit carries a 'Part of #6681' trailer while the PR body carries the ordered 'Fixes #6681' -- push had already happened when I noticed, and force-push is forbidden, so I left it; only the PR body is parsed for closing.",
      "tests": "Union re-run on the FINAL commit d853fdedb with a clean tree (git status --porcelain: 0 entries), all under scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. GATES (all EXIT=0): check-eager-closure-budget, check-control-bytes, check-side-effects-array, check-sdui-registration-pins, check-changeset-presence, check-changeset-fixed, check-changeset-no-major, check-entry-guard, check-node-esm-load --specifiers-only, check-phantom-dependencies. Gate-printed verdict lines: check:eager-closure -- 'Console eager closure is 3171.5 KB gzipped across 45 of 513 chunks (budget: 3222.7 KB, headroom: 51.1 KB).' (baseline run on the same tree before the fix: '3180.2 KB gzipped across 48 of 513 chunks ... headroom: 42.5 KB'); check:sdui-registration-pins -- 'All 12 registration(s) a sideEffects array promises are present in the built console (513 chunks weighed; the 3 ruled control(s) are in the derived set).' including 'marketplace:installed-list 1 chunk(s)', which is the widget my chunk group moved; check:side-effects-array -- '@object-ui/app-shell: sideEffects names exactly the 10 module(s) that register at load time'. BUILD: npx turbo run build --filter=@object-ui/console --concurrency=2 --force, VERDICT command-exit 0, held the lock 251s; its ledger line reads '[plugin declared-lazy-views] 10/12 modules AppContent declares lazy are genuinely lazy; 2 eager, all pinned'. TESTS: npx vitest run --root . --maxWorkers=2 over scripts/__tests__/vite-declared-lazy-views.test.ts + check-eager-closure-budget.test.ts + console-vite-alias-closure-4925.test.ts -- 'Test Files 3 passed (3) / Tests 114 passed (114)', EXIT=0. TYPECHECK: pnpm --filter @object-ui/console type-check EXIT=0 with 'tsc --noEmit && tsc -b tsconfig.node.json --force' echoed, so it was not a zero-match run; npx tsc --noEmit -p tsconfig.scripts.json EXIT=0. LINT NARROWED, and the narrowing is measured: eslint --no-inline-config --format json over the three changed source files reports 'files linted: 3', every errorCount 0, EXIT=0; the fourth changed file is the changeset and eslint.config.js configures no markdown linting; eslint.config.js enables NO type-aware linting (no projectService, no parserOptions.project), so this diff cannot move the verdict on any untouched file. Full farm left to CI. ABLATION (reverse verification of the NEW guard): deleted the metadata-admin line from DECLARED_LAZY_VIEWS_STILL_EAGER and rebuilt to a separate outDir, under a trap on EXIT INT TERM whose restore leg is 'git checkout HEAD -- $REPO_ROOT/scripts/vite-declared-lazy-views.ts' with an ABSOLUTE path. Mutation confirmed ON DISK before the run, not from an editor exit code: git hash-object e1971ad302bf... -> ae9bce55b653..., pin lines 1 -> 0, and the run aborts with 'ablation NOT RUN' if either check fails -- it DID abort on the first attempt, when a perl one-liner silently matched nothing (hash unchanged), which is the whole reason the check exists. On the real run the console build exited 1 in buildStart with '[declared-lazy-views] refusing to declare packages/app-shell/src/views/metadata-admin/index.ts side-effect-free: its own package says otherwise'. Predicted direction was RED and RED is what happened, at buildStart rather than at the later generateBundle ledger half, which is what proves the new guard is reachable. Restore leg proven by observed state, not by exit code: git diff HEAD empty and git hash-object back to e1971ad302bf..., matching git rev-parse HEAD:scripts/vite-declared-lazy-views.ts exactly; dist-ablation removed. No dist was committed (gitignored).",
      "mcp_calls": "4 -- create_pull_request, one failed search_issues (GraphQL rate limit), issue_write for the follow-up, and this report comment. All issue/PR READS went through zero-quota channels (the public issue page's embedded JSON payload, and repo-scoped REST).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6776: metadata-admin's 172,651 B eager closure is held by the ComponentRegistry CONTRACT (registerAppComponent takes a component VALUE, so builtinComponents.tsx must load the pages to register the refs) plus the package barrel's eleven runtime re-exports from a module the sideEffects array declares side-effectful -- not by chunking, and the six pages have dynamicImporters == [] so no advancedChunks group can separate them. Unassigned, labels domain:ui + finding, with the two published-contract questions a grader has to settle first (Suspense coverage at every registry render site; whether the barrel should go on re-exporting runtime values from a side-effectful module).",
        "CHANNEL DECLARATION for that filing, so the dedup read is auditable: the REST search endpoint is blocked by this container's proxy ('sessions are bound to their configured repositories'), and the one targeted MCP search_issues call failed with 'API rate limit already exceeded'. Dedup therefore used the sanctioned repo-scoped REST fallback -- GET /repos/objectstack-ai/objectui/issues?state=open&labels=domain:ui&per_page=100 plus a local grep for metadata-admin / registerAppComponent / eager-closure / builtinComponents / declared-lazy. 100 open domain:ui issues on that single page, NO pagination followed, 10 keyword candidates, none a duplicate (the only eager-closure hit was #6681 itself). The bound is stated rather than hidden: a duplicate outside that one page would not have been seen."
      ]
    }

    Generated by Claude Code

  7. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    ✅ ACCEPT — PR #6775, merged in c6732825d

    Reviewed by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49), round R12. Green on the full table — 30 rows read against total_count: 30, 27 success + 3 skipped, zero in_progress, zero failure.

    ⛔ An error in my dispatch order

    I wrote the path as apps/console/src/views/metadata-admin/index.ts. That file does not exist — the real one is packages/app-shell/src/views/metadata-admin/index.ts. This card's body had the relative spelling right; I absolutized it wrongly. Pure transcription carelessness on my part.

    Two premise corrections, both re-measured rather than inherited

    1. ⭐ One of the four chunks was already fixed before this branch. MarketplacePage is no longer eager at all (this card said 3,129 B), and metadata-admin is 172,651 B, not 181,228 B — faac0d935 / feat(app-shell): the precise sideEffects array and the gate that keeps it honest #6735 got there first. The dev re-measured on its own base instead of differencing against the card, which is exactly the discipline this drift demands.
    2. ⭐ It answered finding: the eager-closure headroom ruling A was decided on (2.02% / ~78 KB) is now 0.905% / 29.3 KB — and I could not establish which side moved #6631's aggregate half: faac0d935 lowered MAX_EAGER_CLOSURE_GZIP_BYTES 3,345,000 → 3,300,000 while removing −51.5 KB of load — both sides moved in one commit, which is why that card could never settle "which side". It correctly did not touch finding: the eager-closure headroom ruling A was decided on (2.02% / ~78 KB) is now 0.905% / 29.3 KB — and I could not establish which side moved #6631 and handed the finding over; it is now recorded there.

    What earns the accept

    All four chunks measured separately from the emitted chunks' own module lists (chunk.modules plus module-level importedIds/importers, from an instrumented build) — ⛔ never a source-level search, per #6680.

    Two were removable, and neither had any import edge to the page at all. Both were held by chunk co-tenancy — #6680's mechanism, not #6535's: rolldown had parked components/SuggestedBindingsPanel.tsx in one and console/marketplace/InstalledListWidget.tsx in the other. An app-shell-eager-leaves group isolates the co-tenants.

    before after
    eager closure 3180.2 KB 3171.5 KB (−8,888 B)
    eager chunks 48 of 513 45 of 513
    headroom 42.5 KB 51.1 KB
    per-chunk ceilings — unmoved

    metadata-admin is NOT removable, and is now pinned with the module list that proves it: the barrel statically imports it for eleven runtime re-exports, services/builtinComponents.tsx registers MetadataDirectoryPage and MetadataResourceRouter by value, and the six declared pages have dynamicImporters == [], so no chunking policy can separate them. ⭐ That is the right shape for a negative result — the proof ships with the pin instead of living in a report.

    ⭐ The ledger widened from 8 to 12 files (every relative lazy() specifier), and the new purity guard reads the package's own sideEffects array — bareSideEffectImport returns null for metadata-admin/index.ts because its five side effects are calls. Without it, deleting the pin would have declared a load-bearing module pure.

    ⭐ Its ablation's disk-confirmation check fired in anger: the first attempt aborted with ablation NOT RUN because a perl one-liner silently matched nothing. That is precisely why the check exists, and it is the difference between an ablation and a hopeful edit.

    ⭐ Its dedup carries a channel declaration with a stated bound — REST search blocked by the proxy, MCP search rate-limited, so a repo-scoped listing of 100 open domain:ui issues plus local grep, no pagination followed, and it says so: "a duplicate outside that one page would not have been seen."

    Filed from this work

    #6776 — metadata-admin's eager closure is held by the ComponentRegistry contract (registerAppComponent takes a component value) plus the barrel's eleven runtime re-exports, not by chunking. Carries the two published-contract questions a grader must settle first.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p1

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions