From d12c111684a123980985c79c0cc241abe80c5bc3 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Sun, 1 Mar 2026 17:07:32 +0800 Subject: [PATCH] perf(export): virtualize session table and prioritize metrics loading --- src/pages/ExportPage.scss | 12 +++- src/pages/ExportPage.tsx | 114 ++++++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 00a5f1e..c576959 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -549,14 +549,19 @@ } .table-wrap { - overflow: auto; + overflow: hidden; border: 1px solid var(--border-color); border-radius: 10px; min-height: 0; flex: 1; } -.session-table { +.table-virtuoso { + height: 100%; +} + +.session-table, +.table-wrap table { width: 100%; min-width: 1300px; border-collapse: separate; @@ -588,7 +593,8 @@ background: rgba(var(--primary-rgb), 0.03); } - .selected-row { + .selected-row, + tbody tr:has(.select-icon-btn.checked) { background: rgba(var(--primary-rgb), 0.08); } diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 0fd53ce..c5338bf 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation } from 'react-router-dom' +import { TableVirtuoso } from 'react-virtuoso' import { Aperture, ChevronDown, @@ -236,6 +237,9 @@ const timestampOrDash = (timestamp?: number): string => { } const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +const METRICS_VIEWPORT_PREFETCH = 140 +const METRICS_BACKGROUND_BATCH = 60 +const METRICS_BACKGROUND_INTERVAL_MS = 180 const WriteLayoutSelector = memo(function WriteLayoutSelector({ writeLayout, @@ -355,6 +359,7 @@ function ExportPage() { const sessionLoadTokenRef = useRef(0) const loadingMetricsRef = useRef>(new Set()) const preselectAppliedRef = useRef(false) + const visibleSessionsRef = useRef([]) useEffect(() => { tasksRef.current = tasks @@ -615,6 +620,10 @@ function ExportPage() { }) }, [sessions, activeTab, searchKeyword, sessionMetrics]) + useEffect(() => { + visibleSessionsRef.current = visibleSessions + }, [visibleSessions]) + const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => { const currentMetrics = sessionMetricsRef.current const pending = targetSessions.filter(session => !currentMetrics[session.username] && !loadingMetricsRef.current.has(session.username)) @@ -674,13 +683,44 @@ function ExportPage() { }, []) useEffect(() => { - const targets = visibleSessions.slice(0, 40) + const keyword = searchKeyword.trim().toLowerCase() + const targets = sessions + .filter((session) => { + if (session.kind !== activeTab) return false + if (!keyword) return true + return ( + (session.displayName || '').toLowerCase().includes(keyword) || + session.username.toLowerCase().includes(keyword) + ) + }) + .sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0)) + .slice(0, METRICS_VIEWPORT_PREFETCH) void ensureSessionMetrics(targets) - }, [visibleSessions, ensureSessionMetrics]) + }, [sessions, activeTab, searchKeyword, ensureSessionMetrics]) + + const handleTableRangeChanged = useCallback((range: { startIndex: number; endIndex: number }) => { + const current = visibleSessionsRef.current + if (current.length === 0) return + const start = Math.max(0, range.startIndex - METRICS_VIEWPORT_PREFETCH) + const end = Math.min(current.length - 1, range.endIndex + METRICS_VIEWPORT_PREFETCH) + if (end < start) return + void ensureSessionMetrics(current.slice(start, end + 1)) + }, [ensureSessionMetrics]) useEffect(() => { if (sessions.length === 0) return - void ensureSessionMetrics(sessions) + let cursor = 0 + const timer = window.setInterval(() => { + if (cursor >= sessions.length) { + window.clearInterval(timer) + return + } + const chunk = sessions.slice(cursor, cursor + METRICS_BACKGROUND_BATCH) + cursor += METRICS_BACKGROUND_BATCH + void ensureSessionMetrics(chunk) + }, METRICS_BACKGROUND_INTERVAL_MS) + + return () => window.clearInterval(timer) }, [sessions, ensureSessionMetrics]) const selectedCount = selectedSessions.size @@ -1294,12 +1334,12 @@ function ExportPage() { ) } - const renderRow = (session: SessionRow) => { + const renderRowCells = (session: SessionRow) => { const metrics = sessionMetrics[session.username] || {} const checked = selectedSessions.has(session.username) return ( - + <>