Skip to content

Commit ac9d115

Browse files
ymkiuxdreamhunter2333
authored andcommitted
perf: 路由页渲染优化与 users 卡片间距 (#321)
* perf: 降低路由页重渲染 * style: users 卡片外边距 * fix: 关闭节流立即刷新 * fix: 透传 streaming 参数 * fix: 节流切换立即刷新
1 parent e644d13 commit ac9d115

File tree

4 files changed

+242
-76
lines changed

4 files changed

+242
-76
lines changed

web/src/components/routes/ClientTypeRoutesContent.tsx

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Used by both global routes and project routes
44
*/
55

6-
import { useState, useMemo } from 'react';
6+
import { useState, useMemo, useRef } from 'react';
77
import { useTranslation } from 'react-i18next';
88
import { Plus, RefreshCw, Zap } from 'lucide-react';
99
import {
@@ -38,7 +38,7 @@ import { useQueryClient } from '@tanstack/react-query';
3838
import { useStreamingRequests } from '@/hooks/use-streaming';
3939
import { getClientName, getClientColor } from '@/components/icons/client-icons';
4040
import { getProviderColor, type ProviderType } from '@/lib/theme';
41-
import type { ClientType, Provider } from '@/lib/transport';
41+
import type { ClientType, Provider, ProviderStats } from '@/lib/transport';
4242
import {
4343
SortableProviderRow,
4444
ProviderRowContent,
@@ -58,6 +58,44 @@ const PROVIDER_TYPE_LABELS: Record<Exclude<ProviderTypeKey, 'custom'>, string> =
5858
codex: 'Codex',
5959
};
6060

61+
function isSameProviderStats(a: ProviderStats, b: ProviderStats): boolean {
62+
return (
63+
a.providerID === b.providerID &&
64+
a.totalRequests === b.totalRequests &&
65+
a.successfulRequests === b.successfulRequests &&
66+
a.failedRequests === b.failedRequests &&
67+
a.successRate === b.successRate &&
68+
a.activeRequests === b.activeRequests &&
69+
a.totalInputTokens === b.totalInputTokens &&
70+
a.totalOutputTokens === b.totalOutputTokens &&
71+
a.totalCacheRead === b.totalCacheRead &&
72+
a.totalCacheWrite === b.totalCacheWrite &&
73+
a.totalCost === b.totalCost
74+
);
75+
}
76+
77+
function useStableProviderStats(stats: Record<number, ProviderStats>) {
78+
const prevRef = useRef<Record<number, ProviderStats>>({});
79+
80+
return useMemo(() => {
81+
const prev = prevRef.current;
82+
const next: Record<number, ProviderStats> = {};
83+
84+
for (const [key, value] of Object.entries(stats)) {
85+
const id = Number(key);
86+
const prevValue = prev[id];
87+
if (prevValue && isSameProviderStats(prevValue, value)) {
88+
next[id] = prevValue;
89+
} else {
90+
next[id] = value;
91+
}
92+
}
93+
94+
prevRef.current = next;
95+
return next;
96+
}, [stats]);
97+
}
98+
6199
interface ClientTypeRoutesContentProps {
62100
clientType: ClientType;
63101
projectID: number; // 0 for global routes
@@ -84,6 +122,7 @@ function ClientTypeRoutesContentInner({
84122
const { t } = useTranslation();
85123
const [activeId, setActiveId] = useState<string | null>(null);
86124
const { data: providerStats = {} } = useProviderStats(clientType, projectID || undefined);
125+
const stableProviderStats = useStableProviderStats(providerStats);
87126
const queryClient = useQueryClient();
88127

89128
// 订阅请求更新事件,确保 providerStats 实时刷新
@@ -102,7 +141,6 @@ function ClientTypeRoutesContentInner({
102141

103142
const { data: allRoutes, isLoading: routesLoading } = useRoutes();
104143
const { data: providers = [], isLoading: providersLoading } = useProviders();
105-
const { countsByProviderAndClient } = useStreamingRequests();
106144

107145
const createRoute = useCreateRoute();
108146
const toggleRoute = useToggleRoute();
@@ -116,42 +154,62 @@ function ClientTypeRoutesContentInner({
116154
return allRoutes?.filter((r) => r.clientType === clientType && r.projectID === projectID) || [];
117155
}, [allRoutes, clientType, projectID]);
118156

157+
const normalizedQuery = useMemo(() => searchQuery.trim().toLowerCase(), [searchQuery]);
158+
159+
const providerById = useMemo(() => {
160+
const map = new Map<number, Provider>();
161+
for (const provider of providers) {
162+
map.set(Number(provider.id), provider);
163+
}
164+
return map;
165+
}, [providers]);
166+
167+
const routeByProviderId = useMemo(() => {
168+
const map = new Map<number, (typeof clientRoutes)[number]>();
169+
for (const route of clientRoutes) {
170+
map.set(Number(route.providerID), route);
171+
}
172+
return map;
173+
}, [clientRoutes]);
174+
119175
// Build provider config items
120176
const items = useMemo((): ProviderConfigItem[] => {
121-
const allItems = providers.map((provider) => {
122-
const route = clientRoutes.find((r) => Number(r.providerID) === Number(provider.id)) || null;
177+
const allItems: ProviderConfigItem[] = [];
178+
179+
for (const route of clientRoutes) {
180+
const provider = providerById.get(Number(route.providerID));
181+
if (!provider) continue;
123182
const isNative = (provider.supportedClientTypes || []).includes(clientType);
124-
return {
183+
allItems.push({
125184
id: `${clientType}-provider-${provider.id}`,
126185
provider,
127186
route,
128-
enabled: route?.isEnabled ?? false,
187+
enabled: route.isEnabled ?? false,
129188
isNative,
130-
};
131-
});
189+
});
190+
}
132191

133-
// Only show providers that have routes
134-
let filteredItems = allItems.filter((item) => item.route);
192+
let filteredItems = allItems;
135193

136194
// Apply search filter
137-
if (searchQuery.trim()) {
138-
const query = searchQuery.toLowerCase();
195+
if (normalizedQuery) {
139196
filteredItems = filteredItems.filter(
140197
(item) =>
141-
item.provider.name.toLowerCase().includes(query) ||
142-
item.provider.type.toLowerCase().includes(query),
198+
item.provider.name.toLowerCase().includes(normalizedQuery) ||
199+
item.provider.type.toLowerCase().includes(normalizedQuery),
143200
);
144201
}
145202

146203
return filteredItems.sort((a, b) => {
147-
if (a.route && b.route) return a.route.position - b.route.position;
148-
if (a.route && !b.route) return -1;
149-
if (!a.route && b.route) return 1;
150-
if (a.isNative && !b.isNative) return -1;
151-
if (!a.isNative && b.isNative) return 1;
204+
const posDiff = (a.route?.position ?? 0) - (b.route?.position ?? 0);
205+
if (posDiff !== 0) return posDiff;
206+
if (a.isNative !== b.isNative) return a.isNative ? -1 : 1;
152207
return a.provider.name.localeCompare(b.provider.name);
153208
});
154-
}, [providers, clientRoutes, clientType, searchQuery]);
209+
}, [clientRoutes, clientType, normalizedQuery, providerById]);
210+
211+
const streamingThrottleMs = items.length > 200 ? 1000 : 0;
212+
const { countsByProviderAndClient } = useStreamingRequests({ throttleMs: streamingThrottleMs });
155213

156214
// Get available providers (without routes yet), grouped by type and sorted alphabetically
157215
const groupedAvailableProviders = useMemo((): Record<ProviderTypeKey, Provider[]> => {
@@ -162,16 +220,14 @@ function ClientTypeRoutesContentInner({
162220
custom: [],
163221
};
164222

165-
let available = providers.filter((p) => {
166-
const hasRoute = clientRoutes.some((r) => Number(r.providerID) === Number(p.id));
167-
return !hasRoute;
168-
});
223+
let available = providers.filter((p) => !routeByProviderId.has(Number(p.id)));
169224

170225
// Apply search filter
171-
if (searchQuery.trim()) {
172-
const query = searchQuery.toLowerCase();
226+
if (normalizedQuery) {
173227
available = available.filter(
174-
(p) => p.name.toLowerCase().includes(query) || p.type.toLowerCase().includes(query),
228+
(p) =>
229+
p.name.toLowerCase().includes(normalizedQuery) ||
230+
p.type.toLowerCase().includes(normalizedQuery),
175231
);
176232
}
177233

@@ -191,14 +247,32 @@ function ClientTypeRoutesContentInner({
191247
}
192248

193249
return groups;
194-
}, [providers, clientRoutes, searchQuery]);
250+
}, [providers, normalizedQuery, routeByProviderId]);
195251

196252
// Check if there are any available providers
197253
const hasAvailableProviders = useMemo(() => {
198254
return PROVIDER_TYPE_ORDER.some((type) => groupedAvailableProviders[type].length > 0);
199255
}, [groupedAvailableProviders]);
200256

201-
const activeItem = activeId ? items.find((item) => item.id === activeId) : null;
257+
const itemsById = useMemo(() => {
258+
const map = new Map<string, ProviderConfigItem>();
259+
for (const item of items) {
260+
map.set(item.id, item);
261+
}
262+
return map;
263+
}, [items]);
264+
265+
const itemIds = useMemo(() => items.map((item) => item.id), [items]);
266+
267+
const itemIndexById = useMemo(() => {
268+
const map = new Map<string, number>();
269+
items.forEach((item, index) => {
270+
map.set(item.id, index);
271+
});
272+
return map;
273+
}, [items]);
274+
275+
const activeItem = activeId ? itemsById.get(activeId) ?? null : null;
202276

203277
const handleToggle = (item: ProviderConfigItem) => {
204278
if (item.route) {
@@ -246,10 +320,11 @@ function ClientTypeRoutesContentInner({
246320

247321
if (!over || active.id === over.id) return;
248322

249-
const oldIndex = items.findIndex((item) => item.id === active.id);
250-
const newIndex = items.findIndex((item) => item.id === over.id);
323+
const oldIndex = itemIndexById.get(active.id as string);
324+
const newIndex = itemIndexById.get(over.id as string);
251325

252-
if (oldIndex === -1 || newIndex === -1) return;
326+
if (oldIndex === undefined || newIndex === undefined) return;
327+
if (oldIndex === newIndex) return;
253328

254329
const newItems = arrayMove(items, oldIndex, newIndex);
255330

@@ -307,7 +382,7 @@ function ClientTypeRoutesContentInner({
307382
onDragEnd={handleDragEnd}
308383
>
309384
<SortableContext
310-
items={items.map((item) => item.id)}
385+
items={itemIds}
311386
strategy={verticalListSortingStrategy}
312387
>
313388
<div className="space-y-2">
@@ -320,7 +395,7 @@ function ClientTypeRoutesContentInner({
320395
streamingCount={
321396
countsByProviderAndClient.get(`${item.provider.id}:${clientType}`) || 0
322397
}
323-
stats={providerStats[item.provider.id]}
398+
stats={stableProviderStats[item.provider.id]}
324399
isToggling={toggleRoute.isPending || createRoute.isPending}
325400
onToggle={() => handleToggle(item)}
326401
onDelete={item.route ? () => handleDeleteRoute(item.route!.id) : undefined}
@@ -333,12 +408,12 @@ function ClientTypeRoutesContentInner({
333408
{activeItem && (
334409
<ProviderRowContent
335410
item={activeItem}
336-
index={items.findIndex((i) => i.id === activeItem.id)}
411+
index={itemIndexById.get(activeItem.id) ?? 0}
337412
clientType={clientType}
338413
streamingCount={
339414
countsByProviderAndClient.get(`${activeItem.provider.id}:${clientType}`) || 0
340415
}
341-
stats={providerStats[activeItem.provider.id]}
416+
stats={stableProviderStats[activeItem.provider.id]}
342417
isToggling={false}
343418
isOverlay
344419
onToggle={() => {}}

0 commit comments

Comments
 (0)