perf(grid): optimize virtual scroll to reduce white flash during fast scrolling

Add buffer, skip-hover, will-change, and CSS contain properties to all
RecycleScroller instances (DataGrid, ConnectionTree, ObjectBrowser,
RedisKeyBrowser). Disable pointer-events during scroll in DataGrid to
reduce repaint overhead.
This commit is contained in:
t8y2 2026-05-12 13:50:03 +08:00
parent 368fa5a102
commit 78184544cf
5 changed files with 78 additions and 5 deletions

View File

@ -755,6 +755,17 @@ function syncHeaderScroll(e: Event) {
}
}
let scrollingTimer = 0;
const isScrolling = ref(false);
function onScrollerScroll(e: Event) {
syncHeaderScroll(e);
if (!isScrolling.value) isScrolling.value = true;
clearTimeout(scrollingTimer);
scrollingTimer = window.setTimeout(() => {
isScrolling.value = false;
}, 120);
}
initColumnWidths();
watch(() => visibleColumns.value.length, initColumnWidths);
watch(
@ -2134,10 +2145,13 @@ defineExpose({
v-else
ref="scrollerRef"
class="data-grid-scroller flex-1 overflow-x-auto overscroll-none"
:class="{ 'is-scrolling': isScrolling }"
:items="displayItems"
:item-size="26"
:buffer="600"
:skip-hover="true"
key-field="id"
@scroll="syncHeaderScroll"
@scroll="onScrollerScroll"
>
<template #default="{ item, index }">
<div
@ -2600,6 +2614,8 @@ defineExpose({
<style scoped>
.data-grid-scroller {
overflow-anchor: none;
will-change: scroll-position;
contain: strict;
}
.data-grid-scroller :deep(.vue-recycle-scroller__item-wrapper) {
@ -2607,6 +2623,14 @@ defineExpose({
overflow: visible;
}
.data-grid-scroller :deep(.vue-recycle-scroller__item-view) {
contain: layout style paint;
}
.data-grid-scroller.is-scrolling :deep(.vue-recycle-scroller__item-view) {
pointer-events: none;
}
.ddl-drawer-resizing {
transition: none;
}

View File

@ -408,7 +408,14 @@ watch(
<div class="truncate">{{ t("objects.schemaColumn") }}</div>
<div v-if="hasComments" class="truncate">{{ t("objects.comment") }}</div>
</div>
<RecycleScroller class="min-h-0 flex-1" :items="filteredRows" :item-size="38" key-field="id">
<RecycleScroller
class="object-browser-scroller min-h-0 flex-1"
:items="filteredRows"
:item-size="38"
:buffer="600"
:skip-hover="true"
key-field="id"
>
<template #default="{ item }">
<div
class="grid h-[38px] cursor-pointer items-center gap-3 border-b px-3 hover:bg-accent/50"
@ -514,3 +521,14 @@ watch(
</div>
</div>
</template>
<style scoped>
.object-browser-scroller {
will-change: scroll-position;
contain: content;
}
.object-browser-scroller :deep(.vue-recycle-scroller__item-view) {
contain: layout style paint;
}
</style>

View File

@ -398,7 +398,15 @@ onMounted(loadKeys);
<Loader2 class="w-3.5 h-3.5 animate-spin" />
<span>{{ t("redis.loadingKeys") }}</span>
</div>
<RecycleScroller v-else class="flex-1" :items="visibleRows" :item-size="28" key-field="id">
<RecycleScroller
v-else
class="redis-key-scroller flex-1"
:items="visibleRows"
:item-size="28"
:buffer="600"
:skip-hover="true"
key-field="id"
>
<template #default="{ item: row }">
<div
class="grid border-b text-xs cursor-pointer hover:bg-accent/50 group"
@ -500,3 +508,14 @@ onMounted(loadKeys);
@confirm="applyDangerAction"
/>
</template>
<style scoped>
.redis-key-scroller {
will-change: scroll-position;
contain: content;
}
.redis-key-scroller :deep(.vue-recycle-scroller__item-view) {
contain: layout style paint;
}
</style>

View File

@ -247,11 +247,12 @@ function onSearchToggle(node: TreeNode) {
</div>
<RecycleScroller
v-if="flatNodes.length > 0 && useVirtualTree"
class="min-h-0 flex-1 overflow-y-auto"
class="connection-tree-scroller min-h-0 flex-1 overflow-y-auto"
:items="flatNodes"
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
:skip-hover="true"
key-field="id"
type-field="type"
flow-mode
@ -284,3 +285,14 @@ function onSearchToggle(node: TreeNode) {
</div>
</div>
</template>
<style scoped>
.connection-tree-scroller {
will-change: scroll-position;
contain: content;
}
.connection-tree-scroller :deep(.vue-recycle-scroller__item-view) {
contain: layout style paint;
}
</style>

View File

@ -1,7 +1,7 @@
import type { TreeNode, TreeNodeType } from "@/types/database";
export const SIDEBAR_TREE_ROW_HEIGHT = 28;
export const SIDEBAR_TREE_SCROLL_BUFFER = SIDEBAR_TREE_ROW_HEIGHT * 48;
export const SIDEBAR_TREE_SCROLL_BUFFER = 600;
export const SIDEBAR_TREE_PRERENDER_COUNT = 48;
export interface FlatTreeNode {