perf: async app startup loading to prevent UI freeze

This commit is contained in:
wonfen
2025-06-13 22:58:55 +08:00
parent 0432cad112
commit 5858f05c13
8 changed files with 425 additions and 120 deletions

View File

@@ -5,3 +5,4 @@
pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/

View File

@@ -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");
// 如果窗口可见,则隐藏
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, "Window is hidden, showing it");
// 如果窗口不可见,则显示
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();
}
}
} else {
// 如果窗口不存在,创建一个新窗口
logging!(
info,
Type::Window,
true,
"Window does not exist, creating a new one"
);
resolve::create_window(true);

View File

@@ -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 });
});
}
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
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
}
});
match timeout(
Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun),
)
.await
{
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
}
} else {
true
}
};
// 2. 系统代理重置
let proxy_task = async {
match timeout(
Duration::from_secs(3),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await
{
Ok(_) => {
log::info!(target: "app", "系统代理已重置");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时");
false
}
}
};
// 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
}
}
};
// 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 {
// 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),
Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun),
)
.await
.is_ok()
} else {
true
}
};
// 2. 顺序执行关键清理
let proxy_res = timeout(
Duration::from_secs(1),
// 2. 系统代理重置
let proxy_task = async {
timeout(
Duration::from_secs(2),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await;
.await
.is_ok()
};
let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await;
// 3. 核心服务停止
let core_task = async {
timeout(Duration::from_secs(2), CoreManager::global().stop_core())
.await
.is_ok()
};
// 3. 平台特定清理
// 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
}

View File

@@ -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");

View File

@@ -11,20 +11,4 @@ impl AsyncHandler {
{
async_runtime::spawn(f())
}
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
async_runtime::spawn_blocking(f)
}
pub fn block_on<F, Fut, R>(f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
async_runtime::block_on(f())
}
}

View File

@@ -142,7 +142,8 @@ pub async fn find_unused_port() -> Result<u16> {
/// 异步方式处理启动后的额外任务
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 = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9998; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
">
<div style="margin-bottom: 20px;">
<div style="width: 40px; height: 40px; border: 3px solid #e3e3e3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">正在加载 Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
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 {
"异步窗口任务开始 (启动已标记完成)"
);
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 事件"
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
if is_show {
let window_clone = newly_created_window.clone();
// 立即显示窗口
let _ = window_clone.show();
let _ = window_clone.set_focus();
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
);
// 异步监控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;
// 每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就绪");
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就绪超时({}秒)强制标记就绪",
"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

View File

@@ -48,7 +48,8 @@ const contexts = [
<UpdateStateProvider />,
];
createRoot(container).render(
const root = createRoot(container);
root.render(
<React.StrictMode>
<ComposeContextProvider contexts={contexts}>
<BaseErrorBoundary>
@@ -61,3 +62,12 @@ createRoot(container).render(
</ComposeContextProvider>
</React.StrictMode>,
);
// 错误处理
window.addEventListener("error", (event) => {
console.error("[main.tsx] 全局错误:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.error("[main.tsx] 未处理的Promise拒绝:", event.reason);
});

View File

@@ -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",
}}
/>
></div>
);
}
if (!routersEles) return null;
if (!routersEles) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: mode === "light" ? "#333" : "#fff",
}}
></div>
);
}
return (
<SWRConfig value={{ errorRetryCount: 3 }}>