Skip to content

chore(console): pin the 43 ineffective dynamic imports to a checked ledger - #6007

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5325-fields-ineffective-dynamic-imports
Aug 24, 2026
Merged

chore(console): pin the 43 ineffective dynamic imports to a checked ledger#6007
yinlianghui merged 1 commit into
mainfrom
claude/issue-5325-fields-ineffective-dynamic-imports

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5325

Measured first, then chose — the card's own instruction. Both endings were on the table; the measurement picked this one, and it matches the maintainer ruling already recorded on the card (2026-08-22, option C).

What the build actually emits, measured on 8d3a5294a

43, not "13+". All 43 come from packages/fields; the three apps/console/src/pages/* warnings a previous round measured (DocPage, DocsLayout, DocsSlug) are gone. Two mechanisms, and the card was right that the shape is not uniform:

static importer that defeats the import()count
packages/fields/src/FieldEditWidget.tsx (+ the barrel) — the grid's inline cell editor, which imports synchronously because a cell edits in place27
packages/fields/src/index.tsx alone — the barrel's own export * from './widgets/*.js'16

Is the lazy path reachable? Yes — and it still cannot pay off

getLazyFieldWidget is public API with two live production consumers (app-shell/src/views/ActionParamDialog.tsx and plugin-grid/src/components/BulkActionDialog.tsx), so deleting the import() calls is off the table.

But the laziness is defeated a second time, one level up, by chunking — #5266's own mechanism. apps/console/vite.config.ts declares an advancedChunks group ui-components whose test matches packages/(components|fields); group assignment overrides a module's async-only reachability, and that chunk is eager because packages/components is reached synchronously from the entry.

Control, re-measured on this branch:packages/fields/src/widgets/MarkdownContent.tsx has a working React.lazy (from widgets/richTextDisplay.tsx), is re-exported by nothing, and has no static importer anywhere in production code — so it emits no warning at all and is absent from the ledger. It still lands in the eager ui-components-CNyoWt-3.js. Counter-probe so EAGER is not a stuck answer: the same walk reports lazy for plugin-map, plugin-charts and plugin-report.

So barrel surgery moves zero bytes — and it is a breaking change besides: 16 MISSING_EXPORT errors, 14 widget classes imported by name across plugin-detail, plugin-form, app-shell and apps/console, plus published consumers. That is exactly what the maintainer ruled out.

What this PR does — promotes the warnings, does not silence them

A one-line if (code === 'INEFFECTIVE_DYNAMIC_IMPORT') return false would satisfy "stop the noise" and be strictly worse than the noise: those 43 lines are currently the only thing in the repo saying the laziness does not work, and a 44th widget joining tomorrow would land in silence.

scripts/vite-ineffective-dynamic-imports.ts holds the ledger of the 43 defeated modules plus a Vite plugin that:

  • replaces the pinned 43 with one summary line, whose 27/16 split is computed from the live build's importers, never pinned, so it cannot drift into a comfortable lie;
  • fails the build on an ineffective dynamic import the ledger does not know about — printing rolldown's own warning text for it first, because whoever is debugging a new one needs the importers named, not my summary;
  • fails the build on a pinned entry that did not fire. This is the counter-probe, and it is why the check cannot quietly go blind: a console build that dies before chunk assignment reports zero of these warnings, and this card already recorded that exact trap — a build killed by 16 MISSING_EXPORT errors reported 0, which reads identically to "fixed".

The module graph is untouched. Option A (stop ui-components claiming packages/fields/**, then remove the static edges) stays on hold as the ruling left it; the ledger header records why, and this PR does not foreclose it — when someone does that work, the ledger shrinks and the build says so.

Verification, all on 6774f83dd

Console vite build: BUILD_EXIT=0, raw INEFFECTIVE_DYNAMIC_IMPORT lines in the log 0, and one summary line in their place:

[plugin ineffective-dynamic-import-ledger] 43 ineffective dynamic imports, all pinned (objectui#5325): 27 via FieldEditWidget.tsx (the inline cell editor), 16 via the @object-ui/fields barrel.

Eager closure byte-identical before and after — 3,310,419 gzipped, 52/508 chunks — as a graph-neutral change must be.

Reverse verification of the gate, four legs. Three through a minimal rolldown build driving the real plugin, one through the real console build:

legresult
pinned set matches the buildexit 0, warning suppressed, summary printed
an unpinned ineffective import (probe, pinned: [])exit 1, rolldown's original warning printed, + .../widget.js named
a pinned entry that did not fire (probe)exit 1, - packages/fields/src/widgets/GhostField.tsx named, with the "distrust is the zero" wording
real console build, ledger ablated by deleting the VectorField.tsx line (mutation confirmed on disk: grep -c 1 to 0, git diff --stat 1 deletion)BUILD_EXIT=1, rolldown's raw VectorField.tsx warning printed, + packages/fields/src/widgets/VectorField.tsx named

The ablation restore leg was re-run to green on 6774f83dd (the readings above are from that restored build), and git status is clean.

Gates: pnpm --filter @object-ui/console type-check exit 0 (which compiles vite.config.ts and scripts/vite-*.ts), pnpm type-check:scripts exit 0, pnpm check:control-bytes / lint:coverage / type-check:coverage all OK, and pnpm exec vitest run over scripts/__tests__/ for scripts-type-check, vitest-invocation-guard, check-eager-closure-budget and the new vite-ineffective-dynamic-imports — 4 files, 86 tests passed. ESLint on the three changed files: 3 files linted, 0 errors, 0 warnings (narrowed; eslint.config.js enables no type-aware linting — no project/projectService — so this diff cannot move the verdict on any untouched file).

Note for review: the fix is outside the dispatched fence, necessarily

The dispatch fenced this to packages/fields/**. Nothing inside that fence can fix it: packages/fields' own build emits zero of these warnings (measured — the whole 34-package dependency build emits none); they exist only in the consumer's bundle, so the only lever is the consumer's build config. The one file changed outside packages/fields is apps/console/vite.config.ts, which already houses the two sibling guards of exactly this genre (assertLazyLinterStaysLazy, emitEagerClosureReport). Disjoint from every concurrent sibling claim (packages/types, packages/components, packages/plugin-view); the two new files under scripts/ are new files, not edits.

The dispatch also carried a standing "do not fix this by suppressing the warning", which the maintainer's own option-C ruling ("stop them reading as noise") has to be reconciled with. The reconciliation is the design above: the signal is not removed, it is made into a gate.


Generated by Claude Code

…edger
Every console `vite build` emitted 43 `INEFFECTIVE_DYNAMIC_IMPORT` warnings,
all from `packages/fields`: each `import()` in the widget-loader map is
defeated by a static edge to the same module, so the laziness is decoration.
The warnings are true and unactionable, and 43 of them scrolling past every
build is how a build log stops being read.
They are promoted, not suppressed. `scripts/vite-ineffective-dynamic-imports.ts`
holds the ledger of defeated modules and a Vite plugin that:
- replaces the pinned 43 with one summary line (27 via `FieldEditWidget.tsx`,
the inline cell editor; 16 via the `@object-ui/fields` barrel), computed from
the live build rather than pinned;
- fails the build on an UNPINNED ineffective dynamic import, printing
rolldown's own warning text for it first;
- fails the build on a pinned entry that did NOT fire. That is the
counter-probe: a console build that dies before chunk assignment reports zero
of these warnings, which reads exactly like "fixed".
The module graph is untouched. Removing the defeating static edges is a
breaking change to `@object-ui/fields` (16 `MISSING_EXPORT` errors, 14 widget
classes imported by name across four packages) and buys zero bytes, because the
`ui-components` `advancedChunks` group claims `packages/fields/**` and overrides
async-only reachability a second time. Control: `MarkdownContent.tsx` has an
undefeated `React.lazy`, no static importer, emits no warning, and still lands
in the eager `ui-components-*.js`; counter-probed against `plugin-map`,
`plugin-charts` and `plugin-report`, which the same walk reports as lazy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3232.9 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-8RqDMlaT.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)504.91KB114.42KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.14KB61.88KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.10KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)4.13KB1.96KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 12:42
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit c0091b8Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5325-fields-ineffective-dynamic-imports branch August 24, 2026 12:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude