Files
clash-verge-rev/src/hooks/use-connection-data.ts
T
Tunglies d203121abe perf(ui): reduce CPU and memory pressure on hot frontend paths
WebSocket hooks (use-connection-data, use-log-data)
- Single-pass merge for connection snapshots; reuse previous row reference
  when upload/download are unchanged so downstream memos can bail out
- Single-allocation cap on closedConnections (no double concat+slice)
- Single-allocation clamp in appendLogs
- Cache dayjs().format result within each 50ms log flush window

Connection table (connection-table.tsx)
- Decouple 5s relative-time tick from columnDefs via shared module-level
  tickStore + memoised RelativeTimeCell so the tick no longer rebuilds
  the table
- RowComponent memo with custom comparator (relies on stable row reference)
- Lift sx objects to module scope
- Stable onShowDetail via ref

App data provider (app-data-provider, app-data-context)
- Split monolithic context into six sub-contexts (Proxies/Rules/ClashConfig/
  System/Uptime/Refreshers); 3s uptime ticks no longer invalidate unrelated
  consumers
- useAppData adapter aggregates sub-contexts; consumer files unchanged
- useStableFn helper stabilises all refresh* callback identities
- Extract useCtx helper to remove repeated null-check boilerplate

Canvas traffic graph (enhanced-canvas-traffic-graph, use-traffic-monitor)
- Lift Y-axis scale (max/min) to drawGraph layer; calculateY allocations
  per frame: ~600 -> 1
- Document visibility gate stops RAF and the setNow interval when the
  window is hidden; appendData unaffected
- Reuse the existing visibilitychange listener instead of adding a second
  one; isDocumentVisibleRef guards animate() inside the frame
- Convert forEach arrow shorthands to block bodies
- Drop unnecessary state-setter deps from a useCallback

Proxy groups (proxy-groups, proxy-render, use-render-list)
- Narrow proxyColItemsMemo dep: headState -> headState.showType
- Per-group fingerprint cache + prevListRef in use-render-list; steady-state
  refetch ticks become O(0) and the list reference stays stable when nothing
  changed
- useStableCallback wraps handleCheckAll / handleLocation
2026-04-19 19:33:23 +08:00

130 lines
3.8 KiB
TypeScript

import { useQueryClient } from '@tanstack/react-query'
import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
const MAX_CLOSED_CONNS_NUM = 500
export const initConnData: ConnectionMonitorData = {
uploadTotal: 0,
downloadTotal: 0,
activeConnections: [],
closedConnections: [],
}
export interface ConnectionMonitorData {
uploadTotal: number
downloadTotal: number
activeConnections: IConnectionsItem[]
closedConnections: IConnectionsItem[]
}
const mergeConnectionSnapshot = (
payload: IConnections,
previous: ConnectionMonitorData = initConnData,
): ConnectionMonitorData => {
const nextConnections = payload.connections ?? []
const previousActive = previous.activeConnections ?? []
const previousClosed = previous.closedConnections ?? []
const nextById = new Map<string, IConnectionsItem>()
for (let i = 0; i < nextConnections.length; i++) {
nextById.set(nextConnections[i].id, nextConnections[i])
}
const carried: IConnectionsItem[] = []
const dropped: IConnectionsItem[] = []
for (let i = 0; i < previousActive.length; i++) {
const prev = previousActive[i]
const next = nextById.get(prev.id)
if (next !== undefined) {
nextById.delete(prev.id)
if (prev.upload === next.upload && prev.download === next.download) {
// Reuse prev reference: row identity stability is the contract Stage 2 memo relies on.
carried.push(prev)
} else {
carried.push({
...next,
curUpload: next.upload - prev.upload,
curDownload: next.download - prev.download,
})
}
} else {
dropped.push(prev)
}
}
const activeConnections: IConnectionsItem[] = carried
for (let i = 0; i < nextConnections.length; i++) {
const conn = nextConnections[i]
if (nextById.has(conn.id)) {
activeConnections.push({
...conn,
curUpload: 0,
curDownload: 0,
})
}
}
const rawClosedLen = previousClosed.length + dropped.length
let closedConnections: IConnectionsItem[]
if (rawClosedLen <= MAX_CLOSED_CONNS_NUM) {
closedConnections = previousClosed.concat(dropped)
} else {
const skipPrev = rawClosedLen - MAX_CLOSED_CONNS_NUM
closedConnections =
skipPrev >= previousClosed.length
? dropped.slice(skipPrev - previousClosed.length)
: previousClosed.slice(skipPrev).concat(dropped)
}
return {
uploadTotal: payload.uploadTotal ?? 0,
downloadTotal: payload.downloadTotal ?? 0,
activeConnections,
closedConnections,
}
}
export const useConnectionData = () => {
const queryClient = useQueryClient()
const { response, refresh, subscriptionCacheKey } =
useMihomoWsSubscription<ConnectionMonitorData>({
storageKey: 'mihomo_connection_date',
buildSubscriptKey: (date) => `getClashConnection-${date}`,
fallbackData: initConnData,
connect: () => MihomoWebSocket.connect_connections(),
throttleMs: 16,
setupHandlers: ({ next, scheduleReconnect }) => ({
handleMessage: (data) => {
if (data.startsWith('Websocket error')) {
next(data)
void scheduleReconnect()
return
}
next(null, (old = initConnData) =>
mergeConnectionSnapshot(JSON.parse(data) as IConnections, old),
)
},
}),
})
const clearClosedConnections = () => {
if (!subscriptionCacheKey) return
queryClient.setQueryData<ConnectionMonitorData>([subscriptionCacheKey], {
uploadTotal: response.data?.uploadTotal ?? 0,
downloadTotal: response.data?.downloadTotal ?? 0,
activeConnections: response.data?.activeConnections ?? [],
closedConnections: [],
})
}
return {
response,
refreshGetClashConnection: refresh,
clearClosedConnections,
}
}