perf: virtualize FormDropdownMenu to reduce DOM nodes and image requests#8476
perf: virtualize FormDropdownMenu to reduce DOM nodes and image requests#8476christian-byrne wants to merge 13 commits intomainfrom
Conversation
- Integrate VirtualGrid into FormDropdownMenu for virtualized rendering - Only render visible items (~20-30) instead of all items (100+) - Add computed properties for grid configuration per layout mode - Extend VirtualGrid slot to provide original item index - Change container from max-h to fixed h for proper virtualization Amp-Thread-ID: https://ampcode.com/threads/T-019c0ca8-be8d-770e-ab31-349937cd2acf Co-authored-by: Amp <amp@ampcode.com>
📝 WalkthroughWalkthroughReplaced direct grid rendering with a measured, virtualized Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Menu as FormDropdownMenu
participant Grid as VirtualGrid
participant Viewport as Viewport
participant ItemComp as FormDropdownMenuItem
User->>Menu: open dropdown with items
Menu->>Menu: compute layoutConfig & gridStyle, map items to virtualItems (with keys)
Menu->>Grid: provide virtualItems, layoutConfig, dimensions
Grid->>Viewport: measure container size & scroll (useElementSize, useScroll)
Viewport-->>Grid: provide element sizes and scroll offsets
Grid->>Grid: calculate visible range (start/end) and spacer heights
Grid->>ItemComp: render visible items with props { item, index }
User->>ItemComp: interact (hover/click)
ItemComp->>Menu: emit selection event (item, index)
Menu-->>User: handle selection
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist. Comment |
🎨 Storybook Build Status✅ Build completed successfully! ⏰ Completed at: 02/04/2026, 12:20:27 PM UTC 🔗 Links🎉 Your Storybook is ready for review! |
🎭 Playwright Tests: ✅ PassedResults: 508 passed, 0 failed, 0 flaky, 8 skipped (Total: 516) 📊 Browser Reports
|
Bundle Size ReportSummary
Category Glance Per-category breakdownApp Entry Points — 22.5 kB (baseline 22.5 kB) • 🔴 +6 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 846 kB (baseline 838 kB) • 🔴 +8.24 kBGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 69 kB (baseline 69 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed Panels & Settings — 410 kB (baseline 410 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 12 added / 12 removed User & Accounts — 16 kB (baseline 16 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed Editors & Dialogs — 3.47 kB (baseline 3.47 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 41.5 kB (baseline 37.8 kB) • 🔴 +3.72 kBReusable component library chunks
Status: 6 added / 5 removed Data & Services — 2.1 MB (baseline 2.1 MB) • 🔴 +54 BStores, services, APIs, and repositories
Status: 11 added / 11 removed Utilities & Hooks — 234 kB (baseline 234 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 12 added / 12 removed Vendor & Third-Party — 9.37 MB (baseline 9.37 MB) • ⚪ 0 BExternal libraries and shared vendor chunks
Other — 7.07 MB (baseline 7.08 MB) • 🟢 -11.7 kBBundles that do not match a named category
Status: 49 added / 50 removed |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue`:
- Around line 79-91: The gridTemplateColumns property in the computed gridStyle
is redundant because VirtualGrid's mergedGridStyle will override it with
repeat(${maxColumns}, minmax(0, 1fr)) when maxColumns is finite (which it always
is here), so remove the gridTemplateColumns key from the gridStyle computed
object (leave display, gap, padding, width intact) to avoid dead code; reference
the gridStyle computed, layoutMode.value conditions, and the
VirtualGrid/mergedGridStyle behavior when making the change.
- Line 30: Replace the non-destructured props assignment const props =
defineProps<Props>() with reactive destructuring so the component accesses props
directly (e.g., const { items, ... } = defineProps<Props>()), then update usages
(notably where items is referenced around the template and the code at the
former line 95) to use items directly; ensure you keep the Props type on
defineProps and preserve reactivity by using the direct destructuring pattern
recommended by the guidelines.
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue
Outdated
Show resolved
Hide resolved
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue
Show resolved
Hide resolved
- Consolidate layout config into single computed with Record type - Use reactive props destructuring per AGENTS.md - Change h-[640px] back to max-h-[640px] for proper sizing - Replace hardcoded zinc color with semantic text-muted-foreground - Add defineSlots to VirtualGrid for type safety - Add unit tests for VirtualGrid and FormDropdownMenu Amp-Thread-ID: https://ampcode.com/threads/T-019c0ca8-be8d-770e-ab31-349937cd2acf Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/components/common/VirtualGrid.test.ts`:
- Around line 74-79: The test currently skips all assertions when
receivedIndices is empty which masks failures; update the assertion around the
receivedIndices checks in VirtualGrid.test (the block using receivedIndices and
the for loop) to explicitly assert that receivedIndices.length > 0 (e.g.,
expect(receivedIndices.length).toBeGreaterThan(0)) before asserting the first
index and the incremental sequence, and keep the existing loop that verifies
receivedIndices[i] === receivedIndices[i-1] + 1 so the test fails if no items
render or indices are non-sequential.
- Around line 84-101: The test "respects maxColumns prop" in VirtualGrid.test.ts
currently only checks for a grid element but doesn't assert that maxColumns: 2
influences layout; update the test that mounts VirtualGrid<TestItem> to read the
rendered element's computed style (or the element.style.gridTemplateColumns) and
assert it contains exactly two column tracks (e.g., two "px" or "fr" entries or
matches a regex for two columns), referencing the mounted wrapper and the grid
element found via wrapper.find(...) and using
getComputedStyle(gridElement.element).gridTemplateColumns (or
gridElement.element.style.gridTemplateColumns) to verify the maxColumns behavior
before calling wrapper.unmount().
In `@src/components/common/VirtualGrid.vue`:
- Around line 60-62: Remove the explicit defineSlots call (defineSlots<{ item:
(props: { item: T & { key: string }; index: number }) => unknown }>() ) from
VirtualGrid.vue; instead rely on the template-declared slot (the "item" slot
already defined at line 14) for slot typing per the coding guideline, so delete
the defineSlots invocation and ensure any TypeScript generics or props
referenced in the template stay intact to preserve type inference for the item
slot.
|
There's some issues right now like the grid not being properly truncated. To confirm: bottleneck is the DOM insertion |
…t recalculation loop Amp-Thread-ID: https://ampcode.com/threads/T-019c176b-ea24-74be-8443-446ec9522c03
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue (1)
83-84:⚠️ Potential issue | 🟠 MajorContainer needs fixed height for proper virtualization.
The PR objectives mention changing from
max-h-[640px]toh-[640px], but the current code still usesmax-h-[640px]. This likely explains the truncation issue you reported—VirtualGrid needs a deterministic container height to calculate the visible range correctly. Withmax-h, the container can shrink based on content, causing virtualization miscalculations.🐛 Proposed fix
<div - class="flex max-h-[640px] w-103 flex-col rounded-lg bg-component-node-background pt-4 outline outline-offset-[-1px] outline-node-component-border" + class="flex h-[640px] w-103 flex-col rounded-lg bg-component-node-background pt-4 outline outline-offset-[-1px] outline-node-component-border" >
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/components/common/VirtualGrid.test.ts`:
- Around line 23-46: The test can pass with zero rendered items and mask
regressions; after mounting VirtualGrid (in VirtualGrid.test.ts) and computing
renderedItems via wrapper.findAll('.test-item'), add an assertion that at least
one item is rendered (e.g., expect(renderedItems.length).toBeGreaterThan(0))
before the existing virtualization assertion that renderedItems.length is less
than items.length; update references around createItems, wrapper and
renderedItems so the test fails if nothing is rendered.
In `@src/components/common/VirtualGrid.vue`:
- Around line 9-15: The slot binding in VirtualGrid.vue uses an explicit prop
binding for item; update the <slot name="item" ...> usage to use the same-name
shorthand for the item prop (replace :item="item" with :item) while leaving the
other prop (:index="state.start + i") unchanged so it follows the repo's
slot-binding convention and matches existing styling in the component rendering
loop.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/components/common/VirtualGrid.test.ts`:
- Around line 134-151: The test "renders empty when no items provided" mounts a
wrapper but never unmounts it, which can leak DOM state; add a call to
wrapper.unmount() at the end of this test (after the expect) to tear down the
mounted component instance, or alternatively introduce a scoped variable and an
afterEach(() => wrapper?.unmount()) cleanup; reference the wrapper variable and
the test name "renders empty when no items provided" to locate where to add
wrapper.unmount().
| @@ -97,7 +131,7 @@ const searchQuery = defineModel<string>('searchQuery') | |||
| :layout="layoutMode" | |||
| @click="emit('item-click', item, index)" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| </VirtualGrid> | |||
There was a problem hiding this comment.
The tooltip for the file name was not displaying properly when scrolling to the bottom, and I've fixed it.
- Fixes a VirtualGrid edge-case where grid gap wasn’t included in the measured row/column step,
causing scrollTop to jitter near the bottom (e.g. 429.5 ↔ 429) and breaking PrimeVue tooltips
(they auto-hide on scroll). - Now measures the actual grid step via offsetTop/offsetLeft and computes spacer heights by whole
rows for stable layout. - Adds a unit test to lock in “row step includes gap” behavior and prevent regressions.
# Conflicts: # src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue # src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/common/VirtualGrid.vue (1)
104-113:⚠️ Potential issue | 🟠 MajorSpacer height undercounts partial rows, which can truncate scroll range.
rowsToHeight(itemsCount)divides bycols, so any remaining items that don’t fill a full row only contribute a fractional height. In a CSS grid, a partially filled row still consumes a full row height, so the bottom spacer becomes too short and items near the end can become unreachable. This matches the “truncation” symptoms noted in the PR discussion.🛠️ Suggested fix
-function rowsToHeight(rows: number): string { - return `${(rows / cols.value) * itemHeight.value}px` -} +function rowsToHeight(itemsCount: number): string { + const rows = Math.ceil(itemsCount / cols.value) + return `${rows * itemHeight.value}px` +}
🧹 Nitpick comments (1)
src/components/common/VirtualGrid.test.ts (1)
9-23: Avoid module-level mutable refs in the vueuse mock.
mockedWidth/Height/ScrollYare mutated across tests at module scope, which can leak state between cases. Usevi.hoisted()to define them for the mock and reset inbeforeEachto keep tests isolated.♻️ Suggested pattern
-import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' -const mockedWidth = ref(400) -const mockedHeight = ref(200) -const mockedScrollY = ref(0) +const { mockedWidth, mockedHeight, mockedScrollY } = vi.hoisted(() => ({ + mockedWidth: ref(400), + mockedHeight: ref(200), + mockedScrollY: ref(0) +})) + +beforeEach(() => { + mockedWidth.value = 400 + mockedHeight.value = 200 + mockedScrollY.value = 0 +})Based on learnings: "Keep module mocks contained; do not use global mutable state within test files; use
vi.hoisted()if necessary for per-test Arrange phase manipulation".
Summary
Virtualize the FormDropdownMenu to only render visible items, fixing slow dropdown performance on cloud.
Changes
VirtualGridintoFormDropdownMenufor virtualized renderingVirtualGridslot to provide original item index for O(1) lookupsmax-h-[640px]to fixedh-[640px]for proper virtualizationReview Focus
:key="layoutMode"to force re-render┆Issue is synchronized with this Notion page by Unito
Summary by CodeRabbit
New Features
Bug Fixes
Style
Tests