Skip to content

perf+fix: keep Monaco off first paint, and unblock the dialect e2e matrix - #391

Merged
huyplb merged 3 commits into
mainfrom
perf/monaco-off-first-paint
Sep 9, 2026
Merged

perf+fix: keep Monaco off first paint, and unblock the dialect e2e matrix#391
huyplb merged 3 commits into
mainfrom
perf/monaco-off-first-paint

Conversation

@huyplb

@huyplb huyplb commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Four things, in the order I found them.

1. The app downloaded 4 MB before it could render

eager payload   4.04 MB -> 1.27 MB raw
                ~1.2 MB -> 350 KB gzip
monaco eager    yes -> no
INEFFECTIVE_DYNAMIC_IMPORT   2 -> 0

2.6 MB of it was Monaco, which is not needed until the SQL Editor opens. The lazy() calls meant to prevent that were decoration: four files also imported the sql-editor barrel statically, and a barrel is one module, so importing insertAtCursor or TYPE_META dragged the editor in with them. Rolldown had printed INEFFECTIVE_DYNAMIC_IMPORT on every build and nobody was reading it.

My first attempt — deep imports past the barrel — was rejected by architecture.test.ts, correctly: a feature may compose another's components but not reach into its lib/. So the barrel got smaller instead. It keeps the light surface other features use and stops re-exporting the four editor views, each of whose consumers already loads it through lazy(() => import(...)).

2. The shared label was extracted but never adopted

15 occurrences across 9 files now use sectionLabelCls. More importantly, surfaces.guard.test.ts fails when a fresh copy of the literal appears anywhere and names the file — the way naming.test.ts already enforces conventions here. A/B'd: reintroducing it in UtilitiesView.tsx fails the test and prints that path.

Its second assertion guards the guard: if adoption ever falls back to a handful, the extraction is protecting nothing and should be reconsidered rather than defended.

3. The dialect e2e matrix has been broken since v3 landed

Control run on 9539a4d711, the commit before #385: 11/11 pass. On current main: 4 failures per dialect. It arrived with v3 and nothing caught it, because e2e does not run in CI.

  • The app lands on Home now, and the page objects reached straight for compare controls gated on activeView === 'sync' && syncPane === 'compare' — two conditions, not one. AppPage.gotoSync() satisfies both.
  • A real layout bug, measured rather than guessed:
BUTTON#source-config-btn   x=512 w=25   pos=static
DIV#connection-chip-source x=229 w=95   pos=static

A static child rendering 190px outside a static parent. The chip is min-w-0 flex-1, so a crowded toolbar collapsed its box to 95px while its label, picker and buttons kept their intrinsic ~210px and spilled sideways — far enough that the "Same DB" pill covered the edit button. Playwright called it "not stable"; a user would call it a button that does nothing.

Fixed at the layout level, not with a z-index: the picker may now shrink (it already truncates) and the chip has a 15rem floor, so a toolbar too narrow to hold it wraps — which it is already flex-wrap for. Worth noting the first change alone moved the button from x=512 to x=334, still outside; the floor is what actually fitted the contents.

postgres, mysql, mariadb, sqlserver: 40 passed, 4 failed.

4. Also verified: the SQL Server size fix from #390

That fix shipped unverified because the container would not start. It turned out I had broken it myself — pruning the cloud agent's git worktree left five containers with bind mounts into a deleted path. Recreated, and the fix is now proven on a live engine with a 6-column, 500-row table:

form rows reported bytes reported
old 3000 540,672
new 500 90,112
truth 500

Exactly 6x, the column count.

Still failing

All four dialects fail the same test — "schema history pane records a Lokee snapshot after migrate" — with sync-pane-history-btn not found after the migrate step. One root cause, not four. I stopped rather than keep patching blind, since I have no evidence yet for why the rail item is absent at that point.

Unit suite 3657 passed, eslint 0 errors, typecheck clean.

🤖 Generated with Claude Code


Note

Low Risk
Mostly bundle splitting, test navigation, and CSS layout; no auth, data, or migration execution logic changes.

Overview
Cuts first-paint JavaScript by stopping the sql-editor feature barrel from re-exporting Monaco-backed views. Static imports of light symbols (TYPE_META, SchemaTreePanel, etc.) had been pulling ~2.6 MB Monaco into the eager graph and nullifying lazy() (Rolldown INEFFECTIVE_DYNAMIC_IMPORT). The barrel now keeps only the small surface; SqlEditor / SqlDiffEditor / SqlEditorView load via deep lazy() imports to their component modules in App.tsx, migration history, object detail, and similar call sites. TopToolbar also deep-imports Lokee helpers instead of the feature barrel.

E2E: Adds idempotent AppPage.gotoSync() (Sync rail + Compare pane) before source/target connection flows, because the app defaults to Home and compare controls mount only when activeView === 'sync' && syncPane === 'compare'.

UI fix: Connection chips get a min-w-[15rem] floor and the saved-connection select can shrink (truncate) so a crowded flex-wrap toolbar no longer collapses the chip and lets the “Same DB” pill cover the edit button.

Consistency: Many forms adopt shared sectionLabelCls from @/shared/components/surfaces, with surfaces.guard.test.ts failing the build if the label literal is copy-pasted again.

Reviewed by Cursor Bugbot for commit 40d0972. Bugbot is set up for automated code reviews on this repo. Configure here.

huyplb and others added 3 commits September 8, 2026 22:31
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
The dialect e2e suites have been failing on every engine since the v3 UI
landed, with `page.click` timeouts that read like the app was broken. A control
run on 9539a4d — the commit before #385 merged — passes 11/11, so this
arrived with v3 and nothing caught it, because e2e does not run in CI.

Two causes, and the second is a real bug users hit.

**The app lands on Home now.** The page objects reached straight for the
compare controls, which are gated on `activeView === 'sync' && syncPane ===
'compare'` — two conditions, not one, and Sync can open on Snapshots. AppPage
grew a `gotoSync()` that satisfies both and is idempotent.

**The source connection chip painted its own controls across the toolbar.**
Measured, not guessed:

    BUTTON#source-config-btn   x=512 w=25   pos=static
    DIV#connection-chip-source x=229 w=95   pos=static

A static child rendering 190px outside a static parent. The chip is
`min-w-0 flex-1`, so a crowded toolbar collapsed its box to 95px while its
label, picker and buttons kept their intrinsic ~210px and spilled sideways —
far enough that the "Same DB" pill covered the edit button. Playwright called
it "not stable"; a person would call it a button that does nothing.

Fixed at the layout level rather than with a z-index: the picker may now shrink
(it already truncates), and the chip has a 15rem floor so a toolbar too narrow
to hold it wraps — which the toolbar is already `flex-wrap` for — instead of
crushing it. The first change alone moved the button from x=512 to x=334, still
outside; the floor is what actually fits the contents in the box.

postgres, mysql, mariadb and sqlserver: 40 passed, 4 failed, and all four
failures are the same remaining test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_81384b48-9481-415a-89be-95e42596bb3c)

@huyplb
huyplb merged commit 85cd9ae into main Sep 9, 2026
12 checks passed
@huyplb
huyplb deleted the perf/monaco-off-first-paint branch September 9, 2026 05:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant