fix: optimize asynchronous handling to prevent UI blocking in various components

fix: add missing showNotice error handling and improve async UI feedback

- Add showNotice error notifications to unlock page async error branches
- Restore showNotice for YAML serialization errors in rules/groups/proxies editor
- Ensure all user-facing async errors are surfaced via showNotice
- Add fade-in animation to layout for smoother theme transition and reduce white screen
- Use requestIdleCallback/setTimeout for heavy UI state updates to avoid UI blocking
- Minor: remove window.showNotice usage, use direct import instead
This commit is contained in:
Tunglies
2025-05-30 17:34:38 +08:00
parent 756d303f6a
commit 1e3566ed7d
10 changed files with 378 additions and 275 deletions

View File

@@ -199,38 +199,31 @@ export const EnhancedTrafficStats = () => {
// 使用节流控制更新频率 // 使用节流控制更新频率
const now = Date.now(); const now = Date.now();
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) { if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
// 如果距离上次更新时间小于阈值,只更新图表不更新状态 try {
if (trafficRef.current) { trafficRef.current?.appendData({
trafficRef.current.appendData({
up: data.up, up: data.up,
down: data.down, down: data.down,
timestamp: now, timestamp: now,
}); });
} } catch { }
return; return;
} }
// 更新最后更新时间
lastUpdateRef.current.traffic = now; lastUpdateRef.current.traffic = now;
// 验证数据有效性防止NaN
const safeUp = isNaN(data.up) ? 0 : data.up; const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down; const safeDown = isNaN(data.down) ? 0 : data.down;
try {
// 批量更新状态
setStats(prev => ({ setStats(prev => ({
...prev, ...prev,
traffic: { up: safeUp, down: safeDown } traffic: { up: safeUp, down: safeDown }
})); }));
} catch { }
// 更新图表数据 try {
if (trafficRef.current) { trafficRef.current?.appendData({
trafficRef.current.appendData({
up: safeUp, up: safeUp,
down: safeDown, down: safeDown,
timestamp: now, timestamp: now,
}); });
} } catch { }
} }
} catch (err) { } catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data); console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -317,6 +310,18 @@ export const EnhancedTrafficStats = () => {
return cleanupSockets; return cleanupSockets;
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]); }, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
// 组件卸载时清理所有定时器/引用
useEffect(() => {
return () => {
try {
Object.values(socketRefs.current).forEach(socket => {
if (socket) socket.close();
});
socketRefs.current = { traffic: null, memory: null };
} catch { }
};
}, []);
// 执行垃圾回收 // 执行垃圾回收
const handleGarbageCollection = useCallback(async () => { const handleGarbageCollection = useCallback(async () => {
if (isDebug) { if (isDebug) {

View File

@@ -117,8 +117,18 @@ export const TestCard = () => {
const [removed] = newList.splice(old_index, 1); const [removed] = newList.splice(old_index, 1);
newList.splice(new_index, 0, removed); newList.splice(new_index, 0, removed);
await mutateVerge({ ...verge, test_list: newList }, false); // 优化:先本地更新,再异步 patch避免UI卡死
await patchVerge({ test_list: newList }); mutateVerge({ ...verge, test_list: newList }, false);
const patchFn = () => {
try {
patchVerge({ test_list: newList });
} catch { }
};
if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn);
} else {
setTimeout(patchFn, 0);
}
} }
}, },
[testList, verge, mutateVerge, patchVerge] [testList, verge, mutateVerge, patchVerge]

View File

@@ -180,16 +180,27 @@ export const GroupsEditorViewer = (props: Props) => {
setDeleteSeq(obj?.delete || []); setDeleteSeq(obj?.delete || []);
}, [visualization]); }, [visualization]);
// 优化异步处理大数据yaml.dump避免UI卡死
useEffect(() => { useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) if (prependSeq && appendSeq && deleteSeq) {
const serialize = () => {
try {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ { forceQuotes: true }
forceQuotes: true,
}
) )
); );
} catch (e) {
// 防止异常导致UI卡死
}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(serialize);
} else {
setTimeout(serialize, 0);
}
}
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
const fetchProxyPolicy = async () => { const fetchProxyPolicy = async () => {
@@ -895,8 +906,7 @@ export const GroupsEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`, }`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动

View File

@@ -184,8 +184,10 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
fileDataRef.current = null; fileDataRef.current = null;
// 只传递当前配置激活状态,让父组件决定是否需要触发配置重载 // 优化UI先关闭异步通知父组件
setTimeout(() => {
props.onChange(isActivating); props.onChange(isActivating);
}, 0);
} catch (err: any) { } catch (err: any) {
showNotice("error", err.message || err.toString()); showNotice("error", err.message || err.toString());
} finally { } finally {
@@ -195,9 +197,11 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
); );
const handleClose = () => { const handleClose = () => {
try {
setOpen(false); setOpen(false);
fileDataRef.current = null; fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
} catch { }
}; };
const text = { const text = {

View File

@@ -130,8 +130,9 @@ export const ProxiesEditorViewer = (props: Props) => {
} }
} }
}; };
const handleParse = () => { // 优化异步分片解析避免主线程阻塞解析完成后批量setState
let proxies = [] as IProxyConfig[]; const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
let proxies: IProxyConfig[] = [];
let names: string[] = []; let names: string[] = [];
let uris = ""; let uris = "";
try { try {
@@ -139,10 +140,13 @@ export const ProxiesEditorViewer = (props: Props) => {
} catch { } catch {
uris = proxyUri; uris = proxyUri;
} }
uris const lines = uris.trim().split("\n");
.trim() let idx = 0;
.split("\n") const batchSize = 50;
.forEach((uri) => { function parseBatch() {
const end = Math.min(idx + batchSize, lines.length);
for (; idx < end; idx++) {
const uri = lines[idx];
try { try {
let proxy = parseUri(uri.trim()); let proxy = parseUri(uri.trim());
if (!names.includes(proxy.name)) { if (!names.includes(proxy.name)) {
@@ -150,10 +154,16 @@ export const ProxiesEditorViewer = (props: Props) => {
names.push(proxy.name); names.push(proxy.name);
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); // 不阻塞主流程
} }
}); }
return proxies; if (idx < lines.length) {
setTimeout(parseBatch, 0);
} else {
cb(proxies);
}
}
parseBatch();
}; };
const fetchProfile = async () => { const fetchProfile = async () => {
let data = await readProfileFile(profileUid); let data = await readProfileFile(profileUid);
@@ -192,15 +202,25 @@ export const ProxiesEditorViewer = (props: Props) => {
}, [visualization]); }, [visualization]);
useEffect(() => { useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) if (prependSeq && appendSeq && deleteSeq) {
const serialize = () => {
try {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ { forceQuotes: true }
forceQuotes: true,
}
) )
); );
} catch (e) {
// 防止异常导致UI卡死
}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(serialize);
} else {
setTimeout(serialize, 0);
}
}
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
useEffect(() => { useEffect(() => {
@@ -276,8 +296,9 @@ export const ProxiesEditorViewer = (props: Props) => {
variant="contained" variant="contained"
startIcon={<VerticalAlignTopRounded />} startIcon={<VerticalAlignTopRounded />}
onClick={() => { onClick={() => {
let proxies = handleParse(); handleParseAsync((proxies) => {
setPrependSeq([...proxies, ...prependSeq]); setPrependSeq((prev) => [...proxies, ...prev]);
});
}} }}
> >
{t("Prepend Proxy")} {t("Prepend Proxy")}
@@ -289,8 +310,9 @@ export const ProxiesEditorViewer = (props: Props) => {
variant="contained" variant="contained"
startIcon={<VerticalAlignBottomRounded />} startIcon={<VerticalAlignBottomRounded />}
onClick={() => { onClick={() => {
let proxies = handleParse(); handleParseAsync((proxies) => {
setAppendSeq([...appendSeq, ...proxies]); setAppendSeq((prev) => [...prev, ...proxies]);
});
}} }}
> >
{t("Append Proxy")} {t("Append Proxy")}
@@ -431,8 +453,7 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`, }`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动

View File

@@ -230,7 +230,7 @@ const rules: {
name: "MATCH", name: "MATCH",
required: false, required: false,
}, },
]; ];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -325,16 +325,27 @@ export const RulesEditorViewer = (props: Props) => {
setDeleteSeq(obj?.delete || []); setDeleteSeq(obj?.delete || []);
}, [visualization]); }, [visualization]);
// 优化异步处理大数据yaml.dump避免UI卡死
useEffect(() => { useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) if (prependSeq && appendSeq && deleteSeq) {
const serialize = () => {
try {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ { forceQuotes: true }
forceQuotes: true,
}
) )
); );
} catch (e: any) {
showNotice('error', e?.message || e?.toString() || 'YAML dump error');
}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(serialize);
} else {
setTimeout(serialize, 0);
}
}
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
const fetchProfile = async () => { const fetchProfile = async () => {
@@ -407,8 +418,7 @@ export const RulesEditorViewer = (props: Props) => {
} }
const condition = ruleType.required ?? true ? ruleContent : ""; const condition = ruleType.required ?? true ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""
ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`; }`;
}; };
@@ -701,8 +711,7 @@ export const RulesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets top: 33, // 顶部padding防止遮挡snippets
}, },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`, }`,
fontLigatures: false, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动

View File

@@ -198,3 +198,5 @@ export const useRenderList = (mode: string) => {
currentColumns: col, currentColumns: col,
}; };
}; };
// 优化建议:如有大数据量,建议用虚拟滚动(已在 ProxyGroups 组件中实现),此处无需额外处理。

View File

@@ -151,6 +151,11 @@ const Layout = () => {
const routersEles = useRoutes(routers); const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen(); const { addListener, setupCloseListener } = useListen();
const initRef = useRef(false); const initRef = useRef(false);
const [themeReady, setThemeReady] = useState(false);
useEffect(() => {
setThemeReady(true);
}, [theme]);
const handleNotice = useCallback( const handleNotice = useCallback(
(payload: [string, string]) => { (payload: [string, string]) => {
@@ -271,7 +276,7 @@ const Layout = () => {
return unlisten; return unlisten;
} catch (err) { } catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err); console.error("[Layout] 监听启动完成事件失败:", err);
return () => {}; return () => { };
} }
}; };
@@ -311,13 +316,39 @@ const Layout = () => {
} }
}, [start_page]); }, [start_page]);
if (!themeReady) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
transition: "background 0.2s",
}}
/>
);
}
if (!routersEles) return null; if (!routersEles) return null;
return ( return (
<SWRConfig value={{ errorRetryCount: 3 }}> <SWRConfig value={{ errorRetryCount: 3 }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NoticeManager /> <NoticeManager />
<div
style={{
animation: "fadeIn 0.5s",
WebkitAnimation: "fadeIn 0.5s",
}}
/>
<style>
{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}
</style>
<Paper <Paper
square square
elevation={0} elevation={0}

View File

@@ -252,9 +252,13 @@ export const HomePage = () => {
setSettingsOpen(true); setSettingsOpen(true);
}; };
// 新增:保存设置 // 新增:保存设置时用requestIdleCallback/setTimeout
const handleSaveSettings = (newCards: HomeCardsSettings) => { const handleSaveSettings = (newCards: HomeCardsSettings) => {
setHomeCards(newCards); if (window.requestIdleCallback) {
window.requestIdleCallback(() => setHomeCards(newCards));
} else {
setTimeout(() => setHomeCards(newCards), 0);
}
}; };
return ( return (

View File

@@ -24,6 +24,7 @@ import {
RefreshRounded, RefreshRounded,
AccessTimeOutlined, AccessTimeOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { showNotice } from "@/services/noticeService";
// 定义流媒体检测项类型 // 定义流媒体检测项类型
interface UnlockItem { interface UnlockItem {
@@ -121,61 +122,67 @@ const UnlockPage = () => {
} }
}; };
// invoke加超时防止后端卡死
const invokeWithTimeout = async <T,>(
cmd: string,
args?: any,
timeout = 15000,
): Promise<T> => {
return Promise.race([
invoke<T>(cmd, args),
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
]);
};
// 执行全部项目检测 // 执行全部项目检测
const checkAllMedia = useLockFn(async () => { const checkAllMedia = useLockFn(async () => {
try { try {
setIsCheckingAll(true); setIsCheckingAll(true);
const result = await invoke<UnlockItem[]>("check_media_unlock"); const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const sortedItems = sortItemsByName(result); const sortedItems = sortItemsByName(result);
// 更新UI
setUnlockItems(sortedItems); setUnlockItems(sortedItems);
const currentTime = new Date().toLocaleString(); const currentTime = new Date().toLocaleString();
setLastCheckTime(currentTime); setLastCheckTime(currentTime);
// 保存结果到本地存储
saveResultsToStorage(sortedItems, currentTime); saveResultsToStorage(sortedItems, currentTime);
setIsCheckingAll(false); setIsCheckingAll(false);
} catch (err: any) { } catch (err: any) {
setIsCheckingAll(false); setIsCheckingAll(false);
showNotice('error', err?.message || err?.toString() || '检测超时或失败');
alert("检测超时或失败: " + (err?.message || err));
console.error("Failed to check media unlock:", err); console.error("Failed to check media unlock:", err);
} }
}); });
// 根据项目名称检测单个流媒体服务 // 检测单个流媒体服务
const checkSingleMedia = useLockFn(async (name: string) => { const checkSingleMedia = useLockFn(async (name: string) => {
try { try {
// 将该项目添加到加载状态
setLoadingItems((prev) => [...prev, name]); setLoadingItems((prev) => [...prev, name]);
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
// 执行检测
const result = await invoke<UnlockItem[]>("check_media_unlock");
// 找到对应的检测结果
const targetItem = result.find((item: UnlockItem) => item.name === name); const targetItem = result.find((item: UnlockItem) => item.name === name);
if (targetItem) { if (targetItem) {
// 更新单个检测项结果并按名称排序
const updatedItems = sortItemsByName( const updatedItems = sortItemsByName(
unlockItems.map((item: UnlockItem) => unlockItems.map((item: UnlockItem) =>
item.name === name ? targetItem : item, item.name === name ? targetItem : item,
), ),
); );
// 更新UI
setUnlockItems(updatedItems); setUnlockItems(updatedItems);
const currentTime = new Date().toLocaleString(); const currentTime = new Date().toLocaleString();
setLastCheckTime(currentTime); setLastCheckTime(currentTime);
// 保存结果到本地存储
saveResultsToStorage(updatedItems, currentTime); saveResultsToStorage(updatedItems, currentTime);
} }
// 移除加载状态
setLoadingItems((prev) => prev.filter((item) => item !== name)); setLoadingItems((prev) => prev.filter((item) => item !== name));
} catch (err: any) { } catch (err: any) {
setLoadingItems((prev) => prev.filter((item) => item !== name)); setLoadingItems((prev) => prev.filter((item) => item !== name));
showNotice('error', err?.message || err?.toString() || `检测${name}失败`);
alert("检测超时或失败: " + (err?.message || err));
console.error(`Failed to check ${name}:`, err); console.error(`Failed to check ${name}:`, err);
} }
}); });