fix: throttle WebSocket subscriptions to prevent UI freeze on profile switch (#6683) (#6686)

Add leading-edge throttle to useMihomoWsSubscription, reduce SWR retry
aggressiveness, and increase WebSocket reconnect delay to prevent event
storms when switching profiles under poor network conditions.
This commit is contained in:
Tunglies 2026-04-02 21:22:30 +08:00 committed by GitHub
parent 3714f0c4c8
commit 824bcc77eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 12 deletions

View File

@ -81,6 +81,7 @@ export const useConnectionData = () => {
buildSubscriptKey: (date) => `getClashConnection-${date}`,
fallbackData: initConnData,
connect: () => MihomoWebSocket.connect_connections(),
throttleMs: 16,
setupHandlers: ({ next, scheduleReconnect }) => ({
handleMessage: (data) => {
if (data.startsWith('Websocket error')) {
@ -89,14 +90,9 @@ export const useConnectionData = () => {
return
}
try {
const parsed = JSON.parse(data) as IConnections
next(null, (old = initConnData) =>
mergeConnectionSnapshot(parsed, old),
)
} catch (error) {
next(error)
}
next(null, (old = initConnData) =>
mergeConnectionSnapshot(JSON.parse(data) as IConnections, old),
)
},
}),
})

View File

@ -15,6 +15,7 @@ export const useMemoryData = () => {
buildSubscriptKey: (date) => `getClashMemory-${date}`,
fallbackData: FALLBACK_MEMORY_USAGE,
connect: () => MihomoWebSocket.connect_memory(),
throttleMs: 500,
setupHandlers: ({ next, scheduleReconnect }) => ({
handleMessage: (data) => {
if (data.startsWith('Websocket error')) {

View File

@ -4,7 +4,7 @@ import { mutate, type MutatorCallback } from 'swr'
import useSWRSubscription from 'swr/subscription'
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
export const RECONNECT_DELAY_MS = 100
export const RECONNECT_DELAY_MS = 1000
type NextFn<T> = (error?: any, data?: T | MutatorCallback<T>) => void
@ -26,6 +26,15 @@ interface UseMihomoWsSubscriptionOptions<T> {
fallbackData: T
connect: () => Promise<MihomoWebSocket>
keepPreviousData?: boolean
/**
* When > 0, coalesce rapid WebSocket messages by wrapping the `next`
* function passed to `setupHandlers`. Only the most recent value is
* flushed, at most once per `throttleMs` milliseconds.
*
* Uses `setTimeout` (not `requestAnimationFrame`) so it keeps working
* when the window is backgrounded or minimized.
*/
throttleMs?: number
setupHandlers: (ctx: HandlerContext<T>) => HandlerResult
}
@ -38,6 +47,7 @@ export const useMihomoWsSubscription = <T>(
fallbackData,
connect,
keepPreviousData = true,
throttleMs,
setupHandlers,
} = options
@ -77,18 +87,59 @@ export const useMihomoWsSubscription = <T>(
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
}
let throttleCleanup: (() => void) | undefined
let wrappedNext: NextFn<T> = next
if (throttleMs && throttleMs > 0) {
let pendingData: T | MutatorCallback<T> | undefined
let hasPending = false
let timerId: ReturnType<typeof setTimeout> | null = null
const flush = () => {
timerId = null
if (hasPending) {
const data = pendingData
pendingData = undefined
hasPending = false
next(undefined, data)
}
}
wrappedNext = (error?: any, data?: T | MutatorCallback<T>) => {
if (error !== undefined && error !== null) {
next(error, data)
return
}
if (!timerId) {
next(undefined, data)
timerId = setTimeout(flush, throttleMs)
} else {
pendingData = data
hasPending = true
}
}
throttleCleanup = () => {
if (timerId) {
clearTimeout(timerId)
timerId = null
}
}
}
const {
handleMessage: handleTextMessage,
onConnected,
cleanup,
} = setupHandlers({
next,
next: wrappedNext,
scheduleReconnect,
isMounted: () => isMounted,
})
const cleanupAll = () => {
clearReconnectTimer()
throttleCleanup?.()
cleanup?.()
void closeSocket()
}

View File

@ -16,6 +16,7 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
buildSubscriptKey: (date) => `getClashTraffic-${date}`,
fallbackData: FALLBACK_TRAFFIC,
connect: () => MihomoWebSocket.connect_traffic(),
throttleMs: 200,
setupHandlers: ({ next, scheduleReconnect }) => ({
handleMessage: (data) => {
if (data.startsWith('Websocket error')) {

View File

@ -23,8 +23,8 @@ export const SWR_SLOW_POLL = {
export const SWR_MIHOMO = {
...SWR_NOT_SMART,
errorRetryInterval: 500,
errorRetryCount: 15,
errorRetryInterval: 2000,
errorRetryCount: 3,
}
export const SWR_EXTERNAL_API = {