refactor: Optimize proxy rendering and layout calculation

This commit is contained in:
wonfen 2025-02-20 14:21:55 +08:00
parent 23d1d210c7
commit 2b534e0d51
3 changed files with 89 additions and 243 deletions

View File

@ -473,7 +473,7 @@ export const ProxyGroups = (props: Props) => {
<div style={{ position: "relative", height: "100%" }}>
<Virtuoso
ref={virtuosoRef}
style={{ height: "calc(100% - 16px)" }}
style={{ height: "calc(100% - 14px)" }}
totalCount={renderList.length}
increaseViewportBy={{ top: 256, bottom: 256 }}
overscan={150}
@ -482,7 +482,7 @@ export const ProxyGroups = (props: Props) => {
scrollerRef.current = ref as Element;
}}
components={{
Footer: () => <div style={{ height: "16px" }} />,
Footer: () => <div style={{ height: "8px" }} />,
}}
itemContent={(index) => (
<ProxyRender

View File

@ -1,5 +1,5 @@
import useSWR from "swr";
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import { useEffect, useMemo, useCallback } from "react";
import { getProxies } from "@/services/api";
import { useVerge } from "@/hooks/use-verge";
import { filterSort } from "./use-filter-sort";
@ -11,7 +11,7 @@ import {
} from "./use-head-state";
export interface IRenderItem {
// 组 head item empty | item col
// 组 | head | item | empty | item col
type: 0 | 1 | 2 | 3 | 4;
key: string;
group: IProxyGroupItem;
@ -19,180 +19,59 @@ export interface IRenderItem {
col?: number;
proxyCol?: IProxyItem[];
headState?: HeadState;
// 新增支持图标和其他元数据
icon?: string;
provider?: string;
testUrl?: string;
}
interface ProxiesData {
groups: IProxyGroupItem[];
global?: IProxyGroupItem;
proxies: any[];
}
// 优化列布局计算
const calculateColumns = (width: number, configCol: number): number => {
if (configCol > 0 && configCol < 6) return configCol;
// 缓存计算结果
const groupCache = new WeakMap<ProxiesData, Map<string, IProxyGroupItem>>();
// 用于追踪缓存的key
const cacheKeys = new Set<ProxiesData>();
export const useRenderList = (mode: string) => {
// 添加用户交互标记
const isUserInteracting = useRef(false);
const interactionTimer = useRef<number | null>(null);
// 添加上一次有效的数据缓存
const [lastValidData, setLastValidData] = useState<ProxiesData | null>(null);
// 添加刷新锁
const refreshLock = useRef(false);
const lastRenderList = useRef<IRenderItem[]>([]);
// 组件卸载时清理
useEffect(() => {
return () => {
if (interactionTimer.current) {
clearTimeout(interactionTimer.current);
}
refreshLock.current = false;
isUserInteracting.current = false;
// 清理 WeakMap 缓存
cacheKeys.forEach((key) => {
groupCache.delete(key);
});
cacheKeys.clear();
};
}, []);
// 优化数据获取函数
const fetchProxies = useCallback(async () => {
try {
if (isUserInteracting.current || refreshLock.current) {
return lastValidData;
}
const data = await getProxies();
// 预处理和缓存组数据
if (data && !groupCache.has(data)) {
const groupMap = new Map();
data.groups.forEach((group) => {
groupMap.set(group.name, group);
});
groupCache.set(data, groupMap);
cacheKeys.add(data);
}
setLastValidData(data);
return data;
} catch (error) {
if (lastValidData) return lastValidData;
throw error;
}
}, [lastValidData]);
const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies",
fetchProxies,
{
refreshInterval: 2000,
dedupingInterval: 1000,
revalidateOnFocus: false,
keepPreviousData: true,
onSuccess: (data) => {
if (!data || isUserInteracting.current) return;
if (proxiesData) {
try {
const groupMap = groupCache.get(proxiesData);
if (!groupMap) return;
const needUpdate = data.groups.some((group: IProxyGroupItem) => {
const oldGroup = groupMap.get(group.name);
if (!oldGroup) return true;
return (
oldGroup.now !== group.now ||
oldGroup.type !== group.type ||
JSON.stringify(oldGroup.all) !== JSON.stringify(group.all)
);
});
if (!needUpdate) {
return;
}
} catch (e) {
console.error("Data comparison error:", e);
return;
}
}
},
},
);
// 优化mutateProxies包装函数
const wrappedMutateProxies = useCallback(async () => {
if (interactionTimer.current) {
clearTimeout(interactionTimer.current);
}
try {
// 立即更新本地状态以响应UI
if (proxiesData) {
const currentGroup = proxiesData.groups.find(
(g) => g.now !== undefined,
);
if (currentGroup) {
const optimisticData = { ...proxiesData };
setLastValidData(optimisticData);
}
}
// 执行实际的更新并等待结果
const result = await mutateProxies();
// 更新最新数据
if (result) {
setLastValidData(result);
}
return result;
} catch (error) {
console.error("Failed to update proxies:", error);
// 发生错误时恢复到之前的状态
if (proxiesData) {
setLastValidData(proxiesData);
}
throw error;
} finally {
// 重置状态
isUserInteracting.current = false;
refreshLock.current = false;
if (interactionTimer.current) {
clearTimeout(interactionTimer.current);
interactionTimer.current = null;
}
}
}, [proxiesData, mutateProxies]);
// 确保初始数据加载后更新lastValidData
useEffect(() => {
if (proxiesData && !lastValidData) {
setLastValidData(proxiesData);
}
}, [proxiesData]);
const { verge } = useVerge();
const { width } = useWindowWidth();
const col = useMemo(() => {
const baseCol = Math.floor(verge?.proxy_layout_column || 6);
if (baseCol >= 6 || baseCol <= 0) {
if (width > 1920) return 5;
if (width > 1450) return 4;
if (width > 1024) return 3;
if (width > 900) return 2;
if (width >= 600) return 2;
return 1;
}
return baseCol;
}, [verge?.proxy_layout_column, width]);
};
// 优化分组逻辑
const groupProxies = <T = any>(list: T[], size: number): T[][] => {
return list.reduce((acc, item) => {
const lastGroup = acc[acc.length - 1];
if (!lastGroup || lastGroup.length >= size) {
acc.push([item]);
} else {
lastGroup.push(item);
}
return acc;
}, [] as T[][]);
};
export const useRenderList = (mode: string) => {
const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
{
refreshInterval: 2000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
},
);
const { verge } = useVerge();
const { width } = useWindowWidth();
const [headStates, setHeadState] = useHeadStateNew();
// 优化初始数据加载
// 计算列数
const col = useMemo(
() => calculateColumns(width, verge?.proxy_layout_column || 6),
[width, verge?.proxy_layout_column],
);
// 确保代理数据加载
useEffect(() => {
if (!proxiesData) return;
const { groups, proxies } = proxiesData;
@ -201,26 +80,31 @@ export const useRenderList = (mode: string) => {
(mode === "rule" && !groups.length) ||
(mode === "global" && proxies.length < 2)
) {
const timer = setTimeout(() => mutateProxies(), 500);
return () => clearTimeout(timer);
setTimeout(() => mutateProxies(), 500);
}
}, [proxiesData, mode, mutateProxies]);
// 优化渲染列表计算
const renderList = useMemo(() => {
const currentData = proxiesData || lastValidData;
if (!currentData) return lastRenderList.current;
// 处理渲染列表
const renderList: IRenderItem[] = useMemo(() => {
if (!proxiesData) return [];
const useRule = mode === "rule" || mode === "script";
const renderGroups =
(useRule && currentData.groups.length
? currentData.groups
: [currentData.global!]) || [];
useRule && proxiesData.groups.length
? proxiesData.groups
: [proxiesData.global!];
const newList = renderGroups.flatMap((group: IProxyGroupItem) => {
const retList = renderGroups.flatMap((group) => {
const headState = headStates[group.name] || DEFAULT_STATE;
const ret: IRenderItem[] = [
{ type: 0, key: group.name, group, headState },
{
type: 0,
key: group.name,
group,
headState,
icon: group.icon,
testUrl: group.testUrl,
},
];
if (headState?.open || !useRule) {
@ -231,26 +115,33 @@ export const useRenderList = (mode: string) => {
headState.sortType,
);
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
ret.push({
type: 1,
key: `head-${group.name}`,
group,
headState,
});
if (!proxies.length) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
return ret;
}
if (col > 1) {
ret.push({
type: 3,
key: `empty-${group.name}`,
group,
headState,
});
} else if (col > 1) {
return ret.concat(
groupList(proxies, col).map((proxyCol) => ({
groupProxies(proxies, col).map((proxyCol) => ({
type: 4,
key: `col-${group.name}-${proxyCol[0].name}`,
group,
headState,
col,
proxyCol,
provider: proxyCol[0].provider,
})),
);
}
} else {
return ret.concat(
proxies.map((proxy) => ({
type: 2,
@ -258,67 +149,22 @@ export const useRenderList = (mode: string) => {
group,
proxy,
headState,
provider: proxy.provider,
})),
);
}
}
return ret;
});
const filteredList = !useRule
? newList.slice(1)
: newList.filter((item) => !item.group.hidden);
lastRenderList.current = filteredList;
return filteredList;
}, [headStates, proxiesData, lastValidData, mode, col]);
// 添加滚动处理
useEffect(() => {
const handleScroll = () => {
if (!isUserInteracting.current) {
isUserInteracting.current = true;
// 清除之前的定时器
if (interactionTimer.current) {
clearTimeout(interactionTimer.current);
}
// 设置新的定时器,在滚动停止后恢复刷新
interactionTimer.current = window.setTimeout(() => {
isUserInteracting.current = false;
// 手动触发一次更新
wrappedMutateProxies();
}, 1000) as unknown as number;
}
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
window.removeEventListener("scroll", handleScroll);
if (interactionTimer.current) {
clearTimeout(interactionTimer.current);
}
};
}, [wrappedMutateProxies]);
if (!useRule) return retList.slice(1);
return retList.filter((item) => !item.group.hidden);
}, [headStates, proxiesData, mode, col]);
return {
renderList,
onProxies: wrappedMutateProxies,
onProxies: mutateProxies,
onHeadState: setHeadState,
currentColumns: col,
};
};
function groupList<T = any>(list: T[], size: number): T[][] {
return list.reduce((p, n) => {
if (!p.length) return [[n]];
const i = p.length - 1;
if (p[i].length < size) {
p[i].push(n);
return p;
}
p.push([n]);
return p;
}, [] as T[][]);
}

View File

@ -377,7 +377,7 @@ const ProfilePage = () => {
sx={{
pl: "10px",
pr: "10px",
height: "94%",
height: "calc(100% - 48px)",
overflowY: "auto",
}}
>
@ -416,7 +416,7 @@ const ProfilePage = () => {
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
<Box sx={{ mt: 1.5 }}>
<Box sx={{ mt: 1.5, mb: "10px" }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<ProfileMore