perf(delay): cache latency updates and smooth proxy list refresh
- track delay as structured updates with TTL-backed cache - batch listener notifications to avoid render storms during checks - surface cached latency in proxy items for quicker, steadier UI feedback
This commit is contained in:
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import delayManager from "@/services/delay";
|
||||
import delayManager, { DelayUpdate } from "@/services/delay";
|
||||
|
||||
interface Props {
|
||||
group: IProxyGroupItem;
|
||||
@@ -24,15 +24,17 @@ export const ProxyItemMini = (props: Props) => {
|
||||
|
||||
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
|
||||
const isPreset = presetList.includes(proxy.name);
|
||||
// -1/<=0 为 不显示
|
||||
// -2 为 loading
|
||||
const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
|
||||
// -1/<=0 为不显示,-2 为 loading
|
||||
const [delayState, setDelayState] = useReducer(
|
||||
(_: DelayUpdate, next: DelayUpdate) => next,
|
||||
{ delay: -1, updatedAt: 0 },
|
||||
);
|
||||
const { verge } = useVerge();
|
||||
const timeout = verge?.default_latency_timeout || 10000;
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return;
|
||||
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||
delayManager.setListener(proxy.name, group.name, setDelayState);
|
||||
|
||||
return () => {
|
||||
delayManager.removeListener(proxy.name, group.name);
|
||||
@@ -41,7 +43,32 @@ export const ProxyItemMini = (props: Props) => {
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
if (!proxy) return;
|
||||
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name);
|
||||
if (cachedUpdate) {
|
||||
setDelayState({ ...cachedUpdate });
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackDelay = delayManager.getDelayFix(proxy, group.name);
|
||||
if (fallbackDelay === -1) {
|
||||
setDelayState({ delay: -1, updatedAt: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedAt = 0;
|
||||
const history = proxy.history;
|
||||
if (history && history.length > 0) {
|
||||
const lastRecord = history[history.length - 1];
|
||||
const parsed = Date.parse(lastRecord.time);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
updatedAt = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
setDelayState({
|
||||
delay: fallbackDelay,
|
||||
updatedAt,
|
||||
});
|
||||
}, [proxy, group.name]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,10 +76,14 @@ export const ProxyItemMini = (props: Props) => {
|
||||
}, [updateDelay]);
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
setDelay(-2);
|
||||
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||
setDelayState({ delay: -2, updatedAt: Date.now() });
|
||||
setDelayState(
|
||||
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
||||
);
|
||||
});
|
||||
|
||||
const delayValue = delayState.delay;
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
dense
|
||||
@@ -69,7 +100,7 @@ export const ProxyItemMini = (props: Props) => {
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||
const showDelay = delay > 0;
|
||||
const showDelay = delayValue > 0;
|
||||
const selectColor = mode === "light" ? primary.main : primary.light;
|
||||
|
||||
return {
|
||||
@@ -181,13 +212,13 @@ export const ProxyItemMini = (props: Props) => {
|
||||
<Box
|
||||
sx={{ ml: 0.5, color: "primary.main", display: isPreset ? "none" : "" }}
|
||||
>
|
||||
{delay === -2 && (
|
||||
{delayValue === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
{!proxy.provider && delay !== -2 && (
|
||||
// provider的节点不支持检测
|
||||
{!proxy.provider && delayValue !== -2 && (
|
||||
// provider 的节点不支持检测
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
@@ -196,7 +227,7 @@ export const ProxyItemMini = (props: Props) => {
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
display: "none", // hover才显示
|
||||
display: "none", // hover 时显示
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
@@ -204,7 +235,7 @@ export const ProxyItemMini = (props: Props) => {
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay >= 0 && (
|
||||
{delayValue >= 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
@@ -214,26 +245,29 @@ export const ProxyItemMini = (props: Props) => {
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={delayManager.formatDelayColor(delay, timeout)}
|
||||
color={delayManager.formatDelayColor(delayValue, timeout)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delayManager.formatDelay(delay, timeout)}
|
||||
{delayManager.formatDelay(delayValue, timeout)}
|
||||
</Widget>
|
||||
)}
|
||||
{proxy.type !== "Direct" && delay !== -2 && delay < 0 && selected && (
|
||||
// 展示已选择的icon
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16, mr: 0.5, display: "block" }}
|
||||
/>
|
||||
)}
|
||||
{proxy.type !== "Direct" &&
|
||||
delayValue !== -2 &&
|
||||
delayValue < 0 &&
|
||||
selected && (
|
||||
// 展示已选择的 icon
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16, mr: 0.5, display: "block" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{group.fixed && group.fixed === proxy.name && (
|
||||
// 展示fixed状态
|
||||
// 展示 fixed 状态
|
||||
<span
|
||||
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
|
||||
title={
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useCallback, useEffect, useReducer } from "react";
|
||||
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import delayManager from "@/services/delay";
|
||||
import delayManager, { DelayUpdate } from "@/services/delay";
|
||||
|
||||
interface Props {
|
||||
group: IProxyGroupItem;
|
||||
@@ -49,14 +49,17 @@ export const ProxyItem = (props: Props) => {
|
||||
|
||||
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
|
||||
const isPreset = presetList.includes(proxy.name);
|
||||
// -1/<=0 为 不显示
|
||||
// -2 为 loading
|
||||
const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
|
||||
// -1/<=0 为不显示,-2 为 loading
|
||||
const [delayState, setDelayState] = useReducer(
|
||||
(_: DelayUpdate, next: DelayUpdate) => next,
|
||||
{ delay: -1, updatedAt: 0 },
|
||||
);
|
||||
const { verge } = useVerge();
|
||||
const timeout = verge?.default_latency_timeout || 10000;
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return;
|
||||
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||
delayManager.setListener(proxy.name, group.name, setDelayState);
|
||||
|
||||
return () => {
|
||||
delayManager.removeListener(proxy.name, group.name);
|
||||
@@ -65,7 +68,32 @@ export const ProxyItem = (props: Props) => {
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
if (!proxy) return;
|
||||
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name);
|
||||
if (cachedUpdate) {
|
||||
setDelayState({ ...cachedUpdate });
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackDelay = delayManager.getDelayFix(proxy, group.name);
|
||||
if (fallbackDelay === -1) {
|
||||
setDelayState({ delay: -1, updatedAt: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedAt = 0;
|
||||
const history = proxy.history;
|
||||
if (history && history.length > 0) {
|
||||
const lastRecord = history[history.length - 1];
|
||||
const parsed = Date.parse(lastRecord.time);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
updatedAt = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
setDelayState({
|
||||
delay: fallbackDelay,
|
||||
updatedAt,
|
||||
});
|
||||
}, [proxy, group.name]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,10 +101,14 @@ export const ProxyItem = (props: Props) => {
|
||||
}, [updateDelay]);
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
setDelay(-2);
|
||||
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||
setDelayState({ delay: -2, updatedAt: Date.now() });
|
||||
setDelayState(
|
||||
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
||||
);
|
||||
});
|
||||
|
||||
const delayValue = delayState.delay;
|
||||
|
||||
return (
|
||||
<ListItem sx={sx}>
|
||||
<ListItemButton
|
||||
@@ -88,7 +120,7 @@ export const ProxyItem = (props: Props) => {
|
||||
({ palette: { mode, primary } }) => {
|
||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||
const selectColor = mode === "light" ? primary.main : primary.light;
|
||||
const showDelay = delay > 0;
|
||||
const showDelay = delayValue > 0;
|
||||
|
||||
return {
|
||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
||||
@@ -145,14 +177,14 @@ export const ProxyItem = (props: Props) => {
|
||||
display: isPreset ? "none" : "",
|
||||
}}
|
||||
>
|
||||
{delay === -2 && (
|
||||
{delayValue === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{!proxy.provider && delay !== -2 && (
|
||||
// provider的节点不支持检测
|
||||
{!proxy.provider && delayValue !== -2 && (
|
||||
// provider 的节点不支持检测
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
@@ -161,7 +193,7 @@ export const ProxyItem = (props: Props) => {
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
display: "none", // hover才显示
|
||||
display: "none", // hover 时显示
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
>
|
||||
@@ -169,7 +201,7 @@ export const ProxyItem = (props: Props) => {
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay > 0 && (
|
||||
{delayValue > 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
@@ -179,19 +211,19 @@ export const ProxyItem = (props: Props) => {
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={delayManager.formatDelayColor(delay, timeout)}
|
||||
color={delayManager.formatDelayColor(delayValue, timeout)}
|
||||
sx={({ palette }) =>
|
||||
!proxy.provider
|
||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{delayManager.formatDelay(delay, timeout)}
|
||||
{delayManager.formatDelay(delayValue, timeout)}
|
||||
</Widget>
|
||||
)}
|
||||
|
||||
{delay !== -2 && delay <= 0 && selected && (
|
||||
// 展示已选择的icon
|
||||
{delayValue !== -2 && delayValue <= 0 && selected && (
|
||||
// 展示已选择的 icon
|
||||
<CheckCircleOutlineRounded
|
||||
className="the-icon"
|
||||
sx={{ fontSize: 16 }}
|
||||
|
||||
Reference in New Issue
Block a user