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

View File

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

View File

@ -4,7 +4,7 @@ import { mutate, type MutatorCallback } from 'swr'
import useSWRSubscription from 'swr/subscription' import useSWRSubscription from 'swr/subscription'
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api' 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 type NextFn<T> = (error?: any, data?: T | MutatorCallback<T>) => void
@ -26,6 +26,15 @@ interface UseMihomoWsSubscriptionOptions<T> {
fallbackData: T fallbackData: T
connect: () => Promise<MihomoWebSocket> connect: () => Promise<MihomoWebSocket>
keepPreviousData?: boolean 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 setupHandlers: (ctx: HandlerContext<T>) => HandlerResult
} }
@ -38,6 +47,7 @@ export const useMihomoWsSubscription = <T>(
fallbackData, fallbackData,
connect, connect,
keepPreviousData = true, keepPreviousData = true,
throttleMs,
setupHandlers, setupHandlers,
} = options } = options
@ -77,18 +87,59 @@ export const useMihomoWsSubscription = <T>(
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS) 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 { const {
handleMessage: handleTextMessage, handleMessage: handleTextMessage,
onConnected, onConnected,
cleanup, cleanup,
} = setupHandlers({ } = setupHandlers({
next, next: wrappedNext,
scheduleReconnect, scheduleReconnect,
isMounted: () => isMounted, isMounted: () => isMounted,
}) })
const cleanupAll = () => { const cleanupAll = () => {
clearReconnectTimer() clearReconnectTimer()
throttleCleanup?.()
cleanup?.() cleanup?.()
void closeSocket() void closeSocket()
} }

View File

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

View File

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