perf(AssetBrowserModal): virtualize asset grid to reduce network requests (#7919)

## Problem

The `AssetBrowserModal` triggers hundreds of network requests when
opened because `AssetGrid.vue` renders all asset cards immediately using
a simple `v-for` loop. Each `AssetCard` loads its thumbnail image,
causing a flood of simultaneous requests.

## Solution

Replace the simple `v-for` with the existing `VirtualGrid` component
(already used in `AssetsSidebarTab.vue` and `ManagerDialogContent.vue`)
to only render visible items plus a small buffer.

## Changes

- **`AssetGrid.vue`**: Use `VirtualGrid` with computed `assetsWithKey`
that adds the required `key` property from `asset.id`
- **`BaseModalLayout.vue`**: Add `flex-1` to content container for
proper height calculation (required for `VirtualGrid` to work)

## Testing

- All 130 asset-related tests pass
- TypeScript and lint checks pass

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7919-perf-AssetBrowserModal-virtualize-asset-grid-to-reduce-network-requests-2e36d73d365081a1be18d0eb33b7ef8a)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-08 19:05:55 -08:00
committed by GitHub
parent 644a8bc60c
commit 51a7654a39
2 changed files with 35 additions and 22 deletions

View File

@@ -82,7 +82,7 @@
{{ contentTitle }}
</h2>
<div
class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
>
<slot name="content"></slot>
</div>

View File

@@ -1,28 +1,21 @@
<template>
<div
data-component-id="AssetGrid"
:class="
cn('grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-4 p-2')
"
class="h-full"
role="grid"
:aria-label="$t('assetBrowser.assetCollection')"
:aria-rowcount="-1"
:aria-colcount="-1"
:aria-setsize="assets.length"
>
<!-- Loading state -->
<div
v-if="loading"
class="col-span-full flex items-center justify-center py-20"
>
<div v-if="loading" class="flex h-full items-center justify-center py-20">
<i
class="icon-[lucide--loader] size-12 animate-spin text-muted-foreground"
/>
</div>
<!-- Empty state -->
<div
v-else-if="assets.length === 0"
class="col-span-full flex flex-col items-center justify-center py-16 text-muted-foreground"
class="flex h-full flex-col items-center justify-center py-16 text-muted-foreground"
>
<i class="mb-4 icon-[lucide--search] size-10" />
<h3 class="mb-2 text-lg font-medium">
@@ -30,24 +23,33 @@
</h3>
<p class="text-sm">{{ $t('assetBrowser.tryAdjustingFilters') }}</p>
</div>
<template v-else>
<AssetCard
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:interactive="true"
@select="$emit('assetSelect', $event)"
/>
</template>
<VirtualGrid
v-else
:items="assetsWithKey"
:grid-style="gridStyle"
:default-item-height="320"
:default-item-width="240"
>
<template #item="{ item }">
<AssetCard
:asset="item"
:interactive="true"
@select="$emit('assetSelect', $event)"
/>
</template>
</VirtualGrid>
</div>
</template>
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed } from 'vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import AssetCard from '@/platform/assets/components/AssetCard.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { cn } from '@/utils/tailwindUtil'
defineProps<{
const { assets } = defineProps<{
assets: AssetDisplayItem[]
loading?: boolean
}>()
@@ -55,4 +57,15 @@ defineProps<{
defineEmits<{
assetSelect: [asset: AssetDisplayItem]
}>()
const assetsWithKey = computed(() =>
assets.map((asset) => ({ ...asset, key: asset.id }))
)
const gridStyle: Partial<CSSProperties> = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(15rem, 1fr))',
gap: '1rem',
padding: '0.5rem'
}
</script>