Skip to content

perf(query): render each result row's data browser link as a ULink - #154

Merged
Upd4ting merged 1 commit into
mainfrom
perf/query-result-row-links
Nov 10, 2026
Merged

Upd4ting merged 1 commit into
mainfrom
perf/query-result-row-links

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Why

The DMS database console should show a query's result without freezing the page. A query with no slice on a table of more than 1,000 rows, such as schemas.metrics.instance().table("readings"), returns the console's 1,000-row cap. When the result is browsable, each row renders its "Open in data browser" link as a Nuxt UI UButton. Each of those buttons resolves its own variant classes and mounts a ULink, a ULinkBase and a UIcon. Mounting 1,000 of them took most of the render.

Change

frontend-vue/app/components/QueryResultPanel.vue: the row link is now a <ULink raw> with the classes the UButton resolved to, wrapping one <UIcon>. It keeps the same role, accessible name, title and href. It still navigates in-app through ULink, and still shows only on row hover or keyboard focus.

Workload

The production dashboard build of an isolated DMS run (dms-control up). Chromium is headless at 1280×900 with 4× CPU throttling (Emulation.setCPUThrottlingRate). It runs schemas.metrics.instance().table("readings").slice(0, 1000) with Ctrl+Enter, after a 1-row query, so the table mounts fresh each time. Timing runs from the keypress to the paint after 1,000 rows exist, with long tasks read from a PerformanceObserver. Base and change ran as two separate DMS runs: 3 rounds of 5 runs per side, interleaved.

Evidence

Confirmed in an end-to-end browser benchmark.

1,000-row result, 4× CPU Before (median, range, n=15) After (median, range, n=15)
Result visible 5,255 ms (4,546 to 6,740) 2,252 ms (2,030 to 5,893)
Long tasks 5,136 ms 2,232 ms
Longest task 3,657 ms 1,195 ms

Primary number: a 1,000-row result is visible after 2.25 s instead of 5.26 s, 57% faster on a mid-range laptop profile.

  • Limiter. The same 1,000 rows with .without("_id"), which renders no row links, show in about 1.1 s. The rest of the remaining time is the per-row ULink and the table cells.
  • Variance. Every round's medians are apart: before 5,488, 5,822 and 4,725 ms, after 2,979, 2,234 and 2,175 ms.
  • When this was measured. The timed commit was ce0901f. After review, the change added two classes to the same element (text-xs active:scale-[0.98]), with no new component or node.

Impact

Anyone who runs a broad query in the console. The longest frozen stretch drops from 3.7 s to 1.2 s at 4× throttling, about 0.9 s to 0.3 s on an unthrottled machine. The impact is medium. It happens on every query that hits the 1,000-row cap.

Proof the link is unchanged

The UButton link (base 9260525) and this branch (be8f22b) were compared in a real browser on shop.products and shop.customers results:

rest      24×24, radius 6px, icon 14×14, font 12px, opacity 0
row hover opacity 1
hover     rgb(11,17,20) on rgb(238,241,243)
pressed   23.52×23.52 (scale 0.98)
focus     outline 3px solid, opacity 1, reached with Tab
href      /modules/database/data?schema=shop&table=products&match=_id%3Asku_lamp
Enter     reaches the data browser with no full reload (window marker kept)

Rows with no _id, and a result that is not browsable, render no link in either build.

  • pnpm run checks: Checks: 8 passed, 0 failed.
  • pnpm test:layer: 19 passed (19) files, 166 passed (166) tests.
  • pnpm test:frontend: Verified 2 modules.
flowchart LR
  R[result row] --> B{browsable and has _id?}
  B -- before --> U[UButton: ULink + ULinkBase + UIcon + own variant classes]
  B -- after --> L[ULink raw + UIcon, fixed classes]
Loading

Readability

The template names the link's classes instead of the button props color, variant and size. They are the classes the DMS button theme resolved to for a ghost, neutral, xs, square button, so a theme change to that button no longer reaches this link.

No overlap with #152.

Verify skill

verify-dms-database on be8f22b, with the features plan lists:

Feature Passed Failed
smoke 1 0
query 15 0
guard 6 1

open-in-data-browser-opens-the-target-table-or-the-row passes. It covers a result row's own link opening the data browser filtered on that row.

The verdict reads FAIL because of one guard journey, a-replace-on-a-selection-changes-every-row-the-dry-run-announced. It is the known product finding in .agents/skills/verify-dms-database/features/guard.md: @antelopejs/mongodb 1.6.2 runs a selection replace as findOneAndReplace. The journey fails on main as well. It counts against this PR only because QueryResultPanel.vue is under the guard feature's globs. This diff does not touch the replace path.

A browsable result rendered one UButton per row for its Open in data browser link. Each UButton builds its own variant classes and wraps a ULink, a ULinkBase and a UIcon, so a 1,000-row result spent most of its render mounting them. A raw ULink with the same classes and icon keeps the link, its label, its in-app navigation and its look.
@Upd4ting
Upd4ting merged commit a02bef3 into main Nov 10, 2026
10 checks passed
@Upd4ting
Upd4ting deleted the perf/query-result-row-links branch November 10, 2026 16:25
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