From d44acde3a831f947770f9a09eba47aed7bde5d30 Mon Sep 17 00:00:00 2001 From: huyplb Date: Tue, 8 Sep 2026 22:31:28 -0600 Subject: [PATCH 1/3] Keep Monaco off first paint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app pulled 4.04 MB of JavaScript before it could render — about 1.2 MB gzipped — and 2.6 MB of that was the Monaco editor, which is not needed until someone opens the SQL Editor. The lazy() calls meant to prevent this were decoration. `features/sql-editor` is loaded through `lazy(() => import(...))` in App.tsx, but the same barrel was *also* imported statically by App.tsx, TopToolbar, CloneTableModal and FileQueryModal — and a barrel is one module, so importing `insertAtCursor` or `TYPE_META` from it dragged the editor in with them. Rolldown had been printing INEFFECTIVE_DYNAMIC_IMPORT on every build; nothing was reading it. The first fix I tried was deep imports past the barrel, which architecture.test correctly rejected: a feature may compose another feature's *components* but not reach into its lib/ or api/. The rule is right and the fix was wrong. So the barrel gets smaller instead. It keeps the light surface other features genuinely use — SchemaTreePanel, WriteConfirmDialog, the clone-table and editor-bridge helpers — and stops re-exporting SqlEditor, SqlDiffEditor, SqlEditorView and FileImportsPanel. Every consumer of those four already loads them through `lazy(() => import(...))`, so pointing that import at the component module costs them nothing, and `components/` is the direction the layering rule allows. eager payload 4.04 MB -> 1.27 MB raw ~1.2 MB -> 350 KB gzip monaco eager yes -> no INEFFECTIVE_DYNAMIC_IMPORT warnings 2 -> 0 Suite 3655 passed, architecture test green, typecheck clean. Co-Authored-By: Claude Opus 5 --- apps/web/src/frontend/App.tsx | 9 +++++++-- apps/web/src/frontend/app/shell/TopToolbar.tsx | 6 +++--- .../migrations/components/MigrationHistory.tsx | 4 +++- .../object-detail/components/ObjectDetailPanel.tsx | 8 ++++++-- apps/web/src/frontend/features/sql-editor/index.ts | 14 ++++++++++---- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/apps/web/src/frontend/App.tsx b/apps/web/src/frontend/App.tsx index 94e565b2..93d3de6f 100644 --- a/apps/web/src/frontend/App.tsx +++ b/apps/web/src/frontend/App.tsx @@ -1,7 +1,10 @@ import React, { Suspense, lazy, useEffect } from 'react'; import { TopToolbar } from '@/app/shell/TopToolbar'; import { ActivityRail } from '@/app/shell/ActivityRail'; -import { SchemaTreePanel } from '@/features/sql-editor'; +// Deep import, not the feature barrel: the barrel re-exports the editor, +// which pulls Monaco (2.6 MB) into the eager graph and makes the lazy() below +// decorative. Rolldown said so — INEFFECTIVE_DYNAMIC_IMPORT. +import { SchemaTreePanel } from '@/features/sql-editor/components/SchemaTreePanel'; import { ObjectDetailPanel } from '@/features/object-detail'; import { ErrorBoundary } from '@/app/shell/ErrorBoundary'; import { LoadingScreen } from '@/app/shell/LoadingScreen'; @@ -21,7 +24,9 @@ const AccessView = lazy(() => import('@/features/access').then((m) => ({ default: m.AccessView })) ); const SqlEditorView = lazy(() => - import('@/features/sql-editor').then((m) => ({ default: m.SqlEditorView })) + import('@/features/sql-editor/components/SqlEditorView').then((m) => ({ + default: m.SqlEditorView, + })) ); const UtilitiesView = lazy(() => import('@/features/utilities').then((m) => ({ default: m.UtilitiesView })) diff --git a/apps/web/src/frontend/app/shell/TopToolbar.tsx b/apps/web/src/frontend/app/shell/TopToolbar.tsx index 89408469..e2d5f149 100644 --- a/apps/web/src/frontend/app/shell/TopToolbar.tsx +++ b/apps/web/src/frontend/app/shell/TopToolbar.tsx @@ -7,17 +7,17 @@ import { ArrowRight, ArrowLeftRight, RefreshCw, AlertCircle, Zap, Settings, KeyR import ProfileMenuDefault, { ProfileMenu as ProfileMenuNamed } from './ProfileMenu'; import { CredentialManager } from '@/features/connections'; import { MigrationHistory } from '@/features/migrations'; -import { TYPE_META, TYPE_ORDER } from '@/features/sql-editor'; +import { TYPE_META, TYPE_ORDER } from '@/features/sql-editor/components/SchemaTreePanel'; import type { DbObjectType } from '@/shared/lib/types'; import { connectionNeedsSecret } from '@/shared/lib/provider-settings'; import { schemaCompareBlocker } from '@/shared/lib/dialect-features'; import { ConnectionModal } from '@/features/connections'; import { PasswordInput } from '@/shared/components/PasswordInput'; import { useAuthStore } from '@/app/store/authStore'; -import { captureSchema } from '@/features/lokee-weave'; +import { captureSchema } from '@/features/lokee-weave/api/lokeeApi'; import { toast } from '@/app/store/toastStore'; import { getSessionPassword, setSessionPassword } from '@/shared/lib/sessionPasswords'; -import { HistoryCompareBar } from '@/features/lokee-weave'; +import { HistoryCompareBar } from '@/features/lokee-weave/components/HistoryCompareBar'; import { BrowseBar } from '@/features/object-detail'; import { ActivityIndicator } from './ActivityIndicator'; import { DiffBriefingChips } from '@/features/schema-diff'; diff --git a/apps/web/src/frontend/features/migrations/components/MigrationHistory.tsx b/apps/web/src/frontend/features/migrations/components/MigrationHistory.tsx index b064abb1..3e1fae7b 100644 --- a/apps/web/src/frontend/features/migrations/components/MigrationHistory.tsx +++ b/apps/web/src/frontend/features/migrations/components/MigrationHistory.tsx @@ -12,7 +12,9 @@ import { type MigrationRunStatus, } from '../api/migrationApi'; -const SqlEditor = lazy(() => import('@/features/sql-editor').then((m) => ({ default: m.SqlEditor }))); +const SqlEditor = lazy(() => + import('@/features/sql-editor/components/SqlEditor').then((m) => ({ default: m.SqlEditor })) +); interface Props { open: boolean; diff --git a/apps/web/src/frontend/features/object-detail/components/ObjectDetailPanel.tsx b/apps/web/src/frontend/features/object-detail/components/ObjectDetailPanel.tsx index e0edf4ff..990ebab0 100644 --- a/apps/web/src/frontend/features/object-detail/components/ObjectDetailPanel.tsx +++ b/apps/web/src/frontend/features/object-detail/components/ObjectDetailPanel.tsx @@ -20,8 +20,12 @@ import { DependencyWarningDialog } from '@/features/object-detail/components/Dep import { ValidationWarningsDialog } from '@/features/object-detail/components/ValidationWarningsDialog'; import { CrossDialectReadinessDialog } from '@/features/object-detail/components/CrossDialectReadinessDialog'; // Monaco is heavy — load it only when a SQL surface is actually shown -const SqlEditor = lazy(() => import('@/features/sql-editor').then((m) => ({ default: m.SqlEditor }))); -const SqlDiffEditor = lazy(() => import('@/features/sql-editor').then((m) => ({ default: m.SqlDiffEditor }))); +const SqlEditor = lazy(() => + import('@/features/sql-editor/components/SqlEditor').then((m) => ({ default: m.SqlEditor })) +); +const SqlDiffEditor = lazy(() => + import('@/features/sql-editor/components/SqlEditor').then((m) => ({ default: m.SqlDiffEditor })) +); const EditorFallback: React.FC = () => (
diff --git a/apps/web/src/frontend/features/sql-editor/index.ts b/apps/web/src/frontend/features/sql-editor/index.ts index e2753c65..6ae56638 100644 --- a/apps/web/src/frontend/features/sql-editor/index.ts +++ b/apps/web/src/frontend/features/sql-editor/index.ts @@ -16,7 +16,13 @@ export { scrubRemovedFileConnections } from './lib/fileQueryEditorHelpers'; export { getCaretOffset, getSelectedSql, insertAtCursor } from './lib/sqlEditorBridge'; export type { SchemaCacheEntry } from './lib/sqlEditorBridge'; export { dialectFkConstraintSupport, dialectIndexSupport, executableSqlStatements, findInboundForeignKeyTables, generateCloneTableSql } from './lib/tableBlueprintSql'; -export { SqlDiffEditor } from './components/SqlEditor'; -export { SqlEditor } from './components/SqlEditor'; -export { SqlEditorView } from './components/SqlEditorView'; -export { FileImportsPanel } from './components/FileImportsPanel'; +// The editor views are deliberately NOT re-exported here. +// +// A barrel is one module: re-exporting them made every consumer of any symbol +// above pull Monaco (2.6 MB) into the eager graph, which is what turned the +// `lazy()` calls in App.tsx and elsewhere into decoration. Rolldown had been +// saying so all along — INEFFECTIVE_DYNAMIC_IMPORT. +// +// Every consumer of these loads them through `lazy(() => import(...))`, so +// importing the component module directly costs them nothing and keeps the +// editor out of first paint. From 6620ed67c6ce7d719371af30a94b5053cd4091ed Mon Sep 17 00:00:00 2001 From: huyplb Date: Tue, 8 Sep 2026 22:35:25 -0600 Subject: [PATCH 2/3] Make the shared label actually shared, and keep it that way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit surfaces.tsx was extracted last week on the strength of a count — the same uppercase micro-label written eight ways across the frontend — and then almost nothing adopted it. Four files imported it; the literal spelling stayed in fifteen more places. An extraction that nobody uses is not a consolidation, it is a ninth variant with its own test file. Fifteen occurrences across nine files now reference `sectionLabelCls` instead of repeating the string. The markup is untouched: this replaces the class literal, not the element, which is the change that cannot alter rendering. The guard is the part that matters. `surfaces.guard.test.ts` fails when a fresh copy of the literal appears anywhere under frontend/, and names the file — the same way naming.test.ts already enforces file naming here rather than leaving it to review. A/B'd: reintroducing the literal in UtilitiesView.tsx fails the test and prints that path. Its second assertion guards the guard. If the number of files using the label ever falls back to a handful, the extraction is protecting nothing and should be reconsidered rather than defended. Suite 3657 passed, eslint 0 errors, typecheck clean. Co-Authored-By: Claude Opus 5 --- .../access/components/AccessGrantsStage.tsx | 3 +- .../components/DbAccessPermissionSections.tsx | 3 +- .../access/components/PermissionBuilder.tsx | 3 +- .../admin/components/AdminAccessPanel.tsx | 3 +- .../features/auth/components/SignupWizard.tsx | 3 +- .../components/DatabaseAccessModal.tsx | 7 +-- .../components/IndexManagementModal.tsx | 9 ++-- .../components/ServerInsightsModal.tsx | 5 +- .../utilities/components/UtilitiesView.tsx | 3 +- .../shared/components/surfaces.guard.test.ts | 50 +++++++++++++++++++ 10 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/frontend/shared/components/surfaces.guard.test.ts diff --git a/apps/web/src/frontend/features/access/components/AccessGrantsStage.tsx b/apps/web/src/frontend/features/access/components/AccessGrantsStage.tsx index d53fe116..7e01deec 100644 --- a/apps/web/src/frontend/features/access/components/AccessGrantsStage.tsx +++ b/apps/web/src/frontend/features/access/components/AccessGrantsStage.tsx @@ -27,6 +27,7 @@ import { type DbAccessConfirmRequest, } from './DbAccessPermissionSections'; import type { DbPrincipal } from '@foxschema/sql'; +import { sectionLabelCls } from '@/shared/components/surfaces'; const PRESET_LABEL: Record, string> = { 'read-only': 'Read only', @@ -184,7 +185,7 @@ export const AccessGrantsStage: React.FC<{ ) : ( <>
- + Presets {(Object.keys(PRESET_LABEL) as Exclude[]).map((p) => ( diff --git a/apps/web/src/frontend/features/access/components/DbAccessPermissionSections.tsx b/apps/web/src/frontend/features/access/components/DbAccessPermissionSections.tsx index 294ee06a..44a6a0e8 100644 --- a/apps/web/src/frontend/features/access/components/DbAccessPermissionSections.tsx +++ b/apps/web/src/frontend/features/access/components/DbAccessPermissionSections.tsx @@ -44,6 +44,7 @@ import { type PermissionRequest, } from '@/features/access/lib/access'; import { useAllSchemaObjects } from '@/features/access/lib/useAllSchemaObjects'; +import { sectionLabelCls } from '@/shared/components/surfaces'; type ActionMode = 'grant' | 'revoke'; @@ -435,7 +436,7 @@ export const DbAccessPermissionSections: React.FC = ({ return (
-
+
Permissions
diff --git a/apps/web/src/frontend/features/access/components/PermissionBuilder.tsx b/apps/web/src/frontend/features/access/components/PermissionBuilder.tsx index 7aaf7122..61e451ef 100644 --- a/apps/web/src/frontend/features/access/components/PermissionBuilder.tsx +++ b/apps/web/src/frontend/features/access/components/PermissionBuilder.tsx @@ -27,6 +27,7 @@ import { useAllSchemaObjects } from '../lib/useAllSchemaObjects'; import { useSyncStore } from '@/app/store/useSyncStore'; import { dialectFeatureReason } from '@/shared/lib/dialect-features'; import type { AccessPrincipalDraft } from '../lib/access-draft'; +import { sectionLabelCls } from '@/shared/components/surfaces'; const PRESET_LABEL: Record = { 'read-only': 'Read only', @@ -450,7 +451,7 @@ export const PermissionBuilder: React.FC<{ * have in mind. */}
- + Apply to all {(Object.keys(PRESET_LABEL) as AccessPreset[]) diff --git a/apps/web/src/frontend/features/admin/components/AdminAccessPanel.tsx b/apps/web/src/frontend/features/admin/components/AdminAccessPanel.tsx index 290c61aa..b5720d2b 100644 --- a/apps/web/src/frontend/features/admin/components/AdminAccessPanel.tsx +++ b/apps/web/src/frontend/features/admin/components/AdminAccessPanel.tsx @@ -33,6 +33,7 @@ import { import { useAuthStore } from '@/app/store/authStore'; import { PasswordInput } from '@/shared/components/PasswordInput'; import { AccessReport } from '@/features/access/components/AccessReport'; +import { sectionLabelCls } from '@/shared/components/surfaces'; type Tab = 'users' | 'roles' | 'users-roles'; @@ -610,7 +611,7 @@ export const AdminAccessPanel: React.FC<{ open: boolean; onClose: () => void }> ) : ( )} - + {group} {/* The count is the point of collapsing: it answers "what diff --git a/apps/web/src/frontend/features/auth/components/SignupWizard.tsx b/apps/web/src/frontend/features/auth/components/SignupWizard.tsx index 3fb4a6c0..3ced620a 100644 --- a/apps/web/src/frontend/features/auth/components/SignupWizard.tsx +++ b/apps/web/src/frontend/features/auth/components/SignupWizard.tsx @@ -10,6 +10,7 @@ import React, { useState } from 'react'; import { Loader2, AlertCircle, Mail, Sparkles } from 'lucide-react'; import { submitSignup, skipSignup } from '../api/signupApi'; import { Brand } from '@/app/shell/Brand'; +import { sectionLabelCls } from '@/shared/components/surfaces'; /** * One-time, skippable first-run prompt: collect a subscriber email when the @@ -76,7 +77,7 @@ export const SignupWizard: React.FC<{ onDone: () => void }> = ({ onDone }) => {