perf(table-overview): 优化大量表搜索渲染性能

- 预计算表概览搜索索引与排序键
- 使用 deferred value 降低搜索输入阻塞
- 限制大结果集首批渲染数量并支持继续加载
- 增加表概览过滤与渲染上限回归测试
This commit is contained in:
Syngnat
2026-04-26 20:42:14 +08:00
parent a06f45da28
commit 3b9116e259
3 changed files with 185 additions and 25 deletions

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import {
TABLE_OVERVIEW_RENDER_BATCH_SIZE,
buildTableOverviewSearchIndex,
filterAndSortTableOverviewRows,
resolveTableOverviewVisibleRows,
} from './tableOverviewFilter';
const buildRows = (count: number) => Array.from({ length: count }, (_, index) => ({
name: `table_${String(index).padStart(4, '0')}`,
comment: index === count - 1 ? 'target table comment' : 'normal table',
rows: index,
dataSize: count - index,
indexSize: 0,
}));
describe('tableOverviewFilter', () => {
it('filters against the full table set before applying the render limit', () => {
const rows = buildRows(1200);
const indexed = buildTableOverviewSearchIndex(rows);
const filtered = filterAndSortTableOverviewRows(indexed, 'target', 'name', 'asc');
expect(filtered).toHaveLength(1);
expect(filtered[0].name).toBe('table_1199');
});
it('caps initially rendered rows for large overview result sets', () => {
const rows = buildRows(1200);
const visible = resolveTableOverviewVisibleRows(rows, TABLE_OVERVIEW_RENDER_BATCH_SIZE);
expect(visible.visibleRows).toHaveLength(TABLE_OVERVIEW_RENDER_BATCH_SIZE);
expect(visible.hiddenCount).toBe(1200 - TABLE_OVERVIEW_RENDER_BATCH_SIZE);
expect(visible.totalCount).toBe(1200);
});
it('sorts with precomputed normalized table names', () => {
const indexed = buildTableOverviewSearchIndex([
{ name: 'z_table', comment: '', rows: 1, dataSize: 10, indexSize: 0 },
{ name: 'A_table', comment: '', rows: 2, dataSize: 5, indexSize: 0 },
]);
expect(filterAndSortTableOverviewRows(indexed, '', 'name', 'asc').map((item) => item.name)).toEqual([
'A_table',
'z_table',
]);
});
});

View File

@@ -0,0 +1,66 @@
export const TABLE_OVERVIEW_RENDER_BATCH_SIZE = 300;
export type TableOverviewSortField = 'name' | 'rows' | 'dataSize';
export type TableOverviewSortOrder = 'asc' | 'desc';
export interface TableOverviewFilterRow {
name: string;
comment?: string;
rows: number;
dataSize: number;
indexSize: number;
}
export interface TableOverviewSearchIndexItem<T extends TableOverviewFilterRow> {
row: T;
searchText: string;
sortName: string;
}
export const buildTableOverviewSearchIndex = <T extends TableOverviewFilterRow>(
rows: T[],
): TableOverviewSearchIndexItem<T>[] => rows.map((row) => ({
row,
searchText: `${row.name}\n${row.comment || ''}`.toLowerCase(),
sortName: row.name.toLowerCase(),
}));
export const filterAndSortTableOverviewRows = <T extends TableOverviewFilterRow>(
indexedRows: TableOverviewSearchIndexItem<T>[],
rawSearchText: string,
sortField: TableOverviewSortField,
sortOrder: TableOverviewSortOrder,
): T[] => {
const keyword = String(rawSearchText || '').trim().toLowerCase();
const matched = keyword
? indexedRows.filter((item) => item.searchText.includes(keyword))
: [...indexedRows];
matched.sort((a, b) => {
let cmp = 0;
if (sortField === 'name') {
cmp = a.sortName.localeCompare(b.sortName);
} else if (sortField === 'rows') {
cmp = a.row.rows - b.row.rows;
} else if (sortField === 'dataSize') {
cmp = a.row.dataSize - b.row.dataSize;
}
return sortOrder === 'asc' ? cmp : -cmp;
});
return matched.map((item) => item.row);
};
export const resolveTableOverviewVisibleRows = <T>(
rows: T[],
rawLimit: number,
): { visibleRows: T[]; hiddenCount: number; totalCount: number } => {
const limit = Number.isFinite(rawLimit) && rawLimit > 0
? Math.min(Math.floor(rawLimit), rows.length)
: Math.min(TABLE_OVERVIEW_RENDER_BATCH_SIZE, rows.length);
return {
visibleRows: rows.slice(0, limit),
hiddenCount: Math.max(0, rows.length - limit),
totalCount: rows.length,
};
};