From 5858f05c131709c1290961d025090740c5a32780 Mon Sep 17 00:00:00 2001 From: wonfen Date: Fri, 13 Jun 2025 22:58:55 +0800 Subject: [PATCH] perf: async app startup loading to prevent UI freeze --- .prettierignore | 1 + src-tauri/src/core/hotkey.rs | 59 ++++++-- src-tauri/src/feat/window.rs | 188 +++++++++++++++++++++---- src-tauri/src/lib.rs | 15 +- src-tauri/src/process/async_handler.rs | 16 --- src-tauri/src/utils/resolve.rs | 173 +++++++++++++++++------ src/main.tsx | 12 +- src/pages/_layout.tsx | 81 +++++++++-- 8 files changed, 425 insertions(+), 120 deletions(-) diff --git a/.prettierignore b/.prettierignore index 34a8e568c..6b1f26e7c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ pnpm-lock.yaml src-tauri/target/ +src-tauri/gen/ diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index aafaca35e..b356ecde2 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -153,30 +153,61 @@ impl Hotkey { "=== Hotkey Dashboard Window Operation Start ===" ); - // 使用 spawn_blocking 来确保在正确的线程上执行 - AsyncHandler::spawn_blocking(|| { - logging!(debug, Type::Hotkey, "Toggle dashboard window visibility"); + // 使用异步操作避免阻塞 + AsyncHandler::spawn(move || async move { + logging!( + debug, + Type::Hotkey, + true, + "Toggle dashboard window visibility" + ); // 检查窗口是否存在 if let Some(window) = handle::Handle::global().get_window() { - // 如果窗口可见,则隐藏它 - if window.is_visible().unwrap_or(false) { - logging!(info, Type::Window, "Window is visible, hiding it"); - let _ = window.hide(); - } else { - // 如果窗口不可见,则显示它 - logging!(info, Type::Window, "Window is hidden, showing it"); - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); + // 如果窗口可见,则隐藏 + match window.is_visible() { + Ok(visible) => { + if visible { + logging!( + info, + Type::Window, + true, + "Window is visible, hiding it" + ); + let _ = window.hide(); + } else { + // 如果窗口不可见,则显示 + logging!( + info, + Type::Window, + true, + "Window is hidden, showing it" + ); + if window.is_minimized().unwrap_or(false) { + let _ = window.unminimize(); + } + let _ = window.show(); + let _ = window.set_focus(); + } + } + Err(e) => { + logging!( + warn, + Type::Window, + true, + "Failed to check window visibility: {}", + e + ); + let _ = window.show(); + let _ = window.set_focus(); } - let _ = window.show(); - let _ = window.set_focus(); } } else { // 如果窗口不存在,创建一个新窗口 logging!( info, Type::Window, + true, "Window does not exist, creating a new one" ); resolve::create_window(true); diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 835410a9c..0b15f8aee 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -49,8 +49,9 @@ pub fn open_or_close_dashboard() { } } -/// 优化的应用退出函数 +/// 异步优化的应用退出函数 pub fn quit() { + use crate::process::AsyncHandler; log::debug!(target: "app", "启动退出流程"); // 获取应用句柄并设置退出标志 @@ -60,54 +61,191 @@ pub fn quit() { // 优先关闭窗口,提供立即反馈 if let Some(window) = handle::Handle::global().get_window() { let _ = window.hide(); + log::info!(target: "app", "窗口已隐藏"); } - // 在单独线程中处理资源清理,避免阻塞主线程 - std::thread::spawn(move || { - let cleanup_result = clean(); - app_handle.exit(match cleanup_result { - true => 0, - false => 1, - }); + // 使用异步任务处理资源清理,避免阻塞 + AsyncHandler::spawn(move || async move { + log::info!(target: "app", "开始异步清理资源"); + let cleanup_result = clean_async().await; + + log::info!(target: "app", "资源清理完成,退出代码: {}", if cleanup_result { 0 } else { 1 }); + app_handle.exit(if cleanup_result { 0 } else { 1 }); }); } -pub fn clean() -> bool { +async fn clean_async() -> bool { use tokio::time::{timeout, Duration}; - let rt = tokio::runtime::Runtime::new().unwrap(); - let cleanup_result = rt.block_on(async { - // 1. 处理TUN模式 - let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) { + + log::info!(target: "app", "开始执行异步清理操作..."); + + // 1. 处理TUN模式 + let tun_task = async { + if Config::verge().data().enable_tun_mode.unwrap_or(false) { let disable_tun = serde_json::json!({ "tun": { "enable": false } }); - timeout( - Duration::from_secs(1), + match timeout( + Duration::from_secs(2), MihomoManager::global().patch_configs(disable_tun), ) .await - .is_ok() + { + Ok(_) => { + log::info!(target: "app", "TUN模式已禁用"); + true + } + Err(_) => { + log::warn!(target: "app", "禁用TUN模式超时"); + false + } + } } else { true - }; + } + }; - // 2. 顺序执行关键清理 - let proxy_res = timeout( - Duration::from_secs(1), + // 2. 系统代理重置 + let proxy_task = async { + match timeout( + Duration::from_secs(3), sysopt::Sysopt::global().reset_sysproxy(), ) - .await; + .await + { + Ok(_) => { + log::info!(target: "app", "系统代理已重置"); + true + } + Err(_) => { + log::warn!(target: "app", "重置系统代理超时"); + false + } + } + }; - let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await; + // 3. 核心服务停止 + let core_task = async { + match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await { + Ok(_) => { + log::info!(target: "app", "核心服务已停止"); + true + } + Err(_) => { + log::warn!(target: "app", "停止核心服务超时"); + false + } + } + }; - // 3. 平台特定清理 + // 4. DNS恢复(仅macOS) + #[cfg(target_os = "macos")] + let dns_task = async { + match timeout(Duration::from_millis(1000), resolve::restore_public_dns()).await { + Ok(_) => { + log::info!(target: "app", "DNS设置已恢复"); + true + } + Err(_) => { + log::warn!(target: "app", "恢复DNS设置超时"); + false + } + } + }; + + // 并行执行所有清理任务 + let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task); + + #[cfg(target_os = "macos")] + let dns_success = dns_task.await; + #[cfg(not(target_os = "macos"))] + let dns_success = true; + + let all_success = tun_success && proxy_success && core_success && dns_success; + + log::info!( + target: "app", + "异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", + tun_success, proxy_success, core_success, dns_success, all_success + ); + + all_success +} + +pub fn clean() -> bool { + use tokio::time::{timeout, Duration}; + + // 使用异步处理 + let rt = tokio::runtime::Runtime::new().unwrap(); + let cleanup_result = rt.block_on(async { + log::info!(target: "app", "开始执行清理操作..."); + + // 1. 处理TUN模式 - 并行执行,减少等待时间 + let tun_task = async { + if Config::verge().data().enable_tun_mode.unwrap_or(false) { + let disable_tun = serde_json::json!({ + "tun": { + "enable": false + } + }); + timeout( + Duration::from_secs(2), + MihomoManager::global().patch_configs(disable_tun), + ) + .await + .is_ok() + } else { + true + } + }; + + // 2. 系统代理重置 + let proxy_task = async { + timeout( + Duration::from_secs(2), + sysopt::Sysopt::global().reset_sysproxy(), + ) + .await + .is_ok() + }; + + // 3. 核心服务停止 + let core_task = async { + timeout(Duration::from_secs(2), CoreManager::global().stop_core()) + .await + .is_ok() + }; + + // 4. DNS恢复(仅macOS) #[cfg(target_os = "macos")] - let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await; + let dns_task = async { + timeout(Duration::from_millis(800), resolve::restore_public_dns()) + .await + .is_ok() + }; - tun_success && proxy_res.is_ok() && core_res.is_ok() + // 并行执行所有清理任务,提高效率 + let (tun_success, proxy_success, core_success) = + tokio::join!(tun_task, proxy_task, core_task); + + #[cfg(target_os = "macos")] + let dns_success = dns_task.await; + #[cfg(not(target_os = "macos"))] + let dns_success = true; + + let all_success = tun_success && proxy_success && core_success && dns_success; + + log::info!( + target: "app", + "清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", + tun_success, proxy_success, core_success, dns_success, all_success + ); + + all_success }); + cleanup_result } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 942de6b86..6bb7f53f0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,17 +90,20 @@ pub fn run() { let _ = utils::dirs::init_portable_flag(); - // 单例检测 - let app_exists: bool = AsyncHandler::block_on(move || async move { + // 异步单例检测 + AsyncHandler::spawn(move || async move { logging!(info, Type::Setup, true, "开始检查单例实例..."); match timeout(Duration::from_secs(3), server::check_singleton()).await { Ok(result) => { if result.is_err() { logging!(info, Type::Setup, true, "检测到已有应用实例运行"); - true + if let Some(app_handle) = AppHandleManager::global().get() { + let _ = app_handle.exit(0); + } else { + std::process::exit(0); + } } else { logging!(info, Type::Setup, true, "未检测到其他应用实例"); - false } } Err(_) => { @@ -110,13 +113,9 @@ pub fn run() { true, "单例检查超时,假定没有其他实例运行" ); - false } } }); - if app_exists { - return; - } #[cfg(target_os = "linux")] std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); diff --git a/src-tauri/src/process/async_handler.rs b/src-tauri/src/process/async_handler.rs index 104e75c7f..c5a809d4e 100644 --- a/src-tauri/src/process/async_handler.rs +++ b/src-tauri/src/process/async_handler.rs @@ -11,20 +11,4 @@ impl AsyncHandler { { async_runtime::spawn(f()) } - - pub fn spawn_blocking(f: F) -> JoinHandle - where - F: FnOnce() -> R + Send + 'static, - R: Send + 'static, - { - async_runtime::spawn_blocking(f) - } - - pub fn block_on(f: F) -> R - where - F: FnOnce() -> Fut, - Fut: Future, - { - async_runtime::block_on(f()) - } } diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index ce76a53fe..576279d70 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -142,7 +142,8 @@ pub async fn find_unused_port() -> Result { /// 异步方式处理启动后的额外任务 pub async fn resolve_setup_async(app_handle: &AppHandle) { - logging!(info, Type::Setup, true, "执行异步设置任务..."); + let start_time = std::time::Instant::now(); + logging!(info, Type::Setup, true, "开始执行异步设置任务..."); if VERSION.get().is_none() { let version = app_handle.package_info().version.to_string(); @@ -230,7 +231,25 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) { logging!(trace, Type::System, true, "初始化热键..."); logging_error!(Type::System, true, hotkey::Hotkey::global().init()); - logging!(info, Type::Setup, true, "异步设置任务完成"); + let elapsed = start_time.elapsed(); + logging!( + info, + Type::Setup, + true, + "异步设置任务完成,耗时: {:?}", + elapsed + ); + + // 如果初始化时间过长,记录警告 + if elapsed.as_secs() > 10 { + logging!( + warn, + Type::Setup, + true, + "异步设置任务耗时较长({:?})", + elapsed + ); + } } /// reset system proxy (异步) @@ -315,7 +334,49 @@ pub fn create_window(is_show: bool) -> bool { .fullscreen(false) .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .min_inner_size(520.0, 520.0) - .visible(false) + .visible(true) // 立即显示窗口,避免用户等待 + .initialization_script(r#" + // 添加非侵入式的加载指示器 + document.addEventListener('DOMContentLoaded', function() { + // 只有在React应用还没有挂载时才显示加载指示器 + if (!document.getElementById('root') || !document.getElementById('root').hasChildNodes()) { + // 创建加载指示器,但不覆盖整个body + const loadingDiv = document.createElement('div'); + loadingDiv.id = 'initial-loading-overlay'; + loadingDiv.innerHTML = ` +
+
+
+
+
正在加载 Clash Verge...
+
+ + `; + document.body.appendChild(loadingDiv); + + // 设置定时器,如果React应用在5秒内没有挂载,移除加载指示器 + setTimeout(() => { + const overlay = document.getElementById('initial-loading-overlay'); + if (overlay) { + overlay.remove(); + } + }, 5000); + } + }); + "#) .build() { Ok(newly_created_window) => { @@ -334,72 +395,98 @@ pub fn create_window(is_show: bool) -> bool { "异步窗口任务开始 (启动已标记完成)" ); + // 先运行轻量模式检测 + lightweight::run_once_auto_lightweight(); + + // 发送启动完成事件,触发前端开始加载 + logging!( + debug, + Type::Window, + true, + "发送 verge://startup-completed 事件" + ); + handle::Handle::notify_startup_completed(); + if is_show { let window_clone = newly_created_window.clone(); - // Attempt to show and focus the window first. + // 立即显示窗口 let _ = window_clone.show(); let _ = window_clone.set_focus(); - logging!(debug, Type::Window, true, "窗口已尝试显示和聚焦"); - - lightweight::run_once_auto_lightweight(); - - tokio::time::sleep(Duration::from_millis(100)).await; // Crucial delay - - logging!( - debug, - Type::Window, - true, - "延时后,尝试发送 verge://startup-completed 事件" - ); - handle::Handle::notify_startup_completed(); + logging!(info, Type::Window, true, "窗口已立即显示"); let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() { - 2 + 3 } else { - 5 + 8 }; logging!( info, Type::Window, true, - "等待UI就绪 (最多{}秒)...", + "开始监控UI加载状态 (最多{}秒)...", timeout_seconds ); - let wait_result = - tokio::time::timeout(Duration::from_secs(timeout_seconds), async { - while !*get_ui_ready().read() { - tokio::time::sleep(Duration::from_millis(100)).await; - } - }) - .await; + // 异步监控UI状态,不影响窗口显示 + tokio::spawn(async move { + let wait_result = + tokio::time::timeout(Duration::from_secs(timeout_seconds), async { + let mut check_count = 0; + while !*get_ui_ready().read() { + tokio::time::sleep(Duration::from_millis(100)).await; + check_count += 1; - match wait_result { - Ok(_) => { - logging!(info, Type::Window, true, "UI就绪"); + // 每2秒记录一次等待状态 + if check_count % 20 == 0 { + logging!( + debug, + Type::Window, + true, + "UI加载状态检查... ({}秒)", + check_count / 10 + ); + } + } + }) + .await; + + match wait_result { + Ok(_) => { + logging!(info, Type::Window, true, "UI已完全加载就绪"); + // 移除初始加载指示器 + if let Some(window) = handle::Handle::global().get_window() { + let _ = window.eval(r#" + const overlay = document.getElementById('initial-loading-overlay'); + if (overlay) { + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 300); + } + "#); + } + } + Err(_) => { + logging!( + warn, + Type::Window, + true, + "UI加载监控超时({}秒),但窗口已正常显示", + timeout_seconds + ); + *get_ui_ready().write() = true; + } } - Err(_) => { - logging!( - warn, - Type::Window, - true, - "等待UI就绪超时({}秒),强制标记就绪", - timeout_seconds - ); - *get_ui_ready().write() = true; - } - } + }); + logging!(info, Type::Window, true, "窗口显示流程完成"); } else { logging!( debug, Type::Window, true, - "is_show为false,窗口保持隐藏。尝试发送启动事件。" + "is_show为false,窗口保持隐藏状态" ); - handle::Handle::notify_startup_completed(); } }); true diff --git a/src/main.tsx b/src/main.tsx index 0e95524ff..d1c308064 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -48,7 +48,8 @@ const contexts = [ , ]; -createRoot(container).render( +const root = createRoot(container); +root.render( @@ -61,3 +62,12 @@ createRoot(container).render( , ); + +// 错误处理 +window.addEventListener("error", (event) => { + console.error("[main.tsx] 全局错误:", event.error); +}); + +window.addEventListener("unhandledrejection", (event) => { + console.error("[main.tsx] 未处理的Promise拒绝:", event.reason); +}); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 5af05e31e..57f0730b8 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -288,24 +288,61 @@ const Layout = () => { } }; - // 初始阶段 - 开始加载 - notifyUiStage("Loading"); + // 简化的UI初始化流程 + const initializeUI = async () => { + try { + const initialOverlay = document.getElementById( + "initial-loading-overlay", + ); + if (initialOverlay) { + initialOverlay.style.opacity = "0"; + setTimeout(() => initialOverlay.remove(), 200); + } - setTimeout(() => { - notifyUiCoreReady(); + await notifyUiStage("Loading"); - setTimeout(() => { - notifyUiResourcesLoaded(); - setTimeout(() => { - notifyUiReady(); - }, 100); - }, 100); - }, 100); + await new Promise((resolve) => setTimeout(resolve, 100)); + + console.log("[Layout] 通知后端:DomReady"); + await notifyUiCoreReady(); + + await new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }); + + console.log("[Layout] 通知后端:ResourcesLoaded"); + await notifyUiResourcesLoaded(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + await notifyUiReady(); + } catch (error) { + try { + await notifyUiReady(); + } catch (e) { + console.error("[Layout] 通知UI就绪失败:", e); + } + } + }; + + setTimeout(initializeUI, 50); + + const emergencyTimeout = setTimeout(() => { + const emergencyNotify = async () => { + try { + await invoke("notify_ui_ready"); + } catch (error) {} + }; + emergencyNotify(); + }, 5000); // 启动监听器 const unlistenPromise = listenStartupCompleted(); return () => { + clearTimeout(emergencyTimeout); unlistenPromise.then((unlisten) => unlisten()); }; }, []); @@ -332,12 +369,30 @@ const Layout = () => { height: "100vh", background: mode === "light" ? "#fff" : "#181a1b", transition: "background 0.2s", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: mode === "light" ? "#333" : "#fff", }} - /> + > ); } - if (!routersEles) return null; + if (!routersEles) { + return ( +
+ ); + } return (