mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(windows): idle WebView2 when Tauri windows are hidden (#273)
* fix(windows): stop GPU rendering when windows are hidden
* perf(ui): tighten hidden-window rendering mitigation
Align implementation and documentation with what the injected pause/resume
scripts actually do, reduce main-thread and compositor wakeups while windows
are invisible, and make lifecycle cleanup reliable.
Rust (Tauri)
- Document PAUSE/RESUME as compositor-oriented hints: set __psyHidden, optional
--psy-anim-speed for CSS that opts in, and pause @keyframes via
animation-play-state only (not CSS transitions, not arbitrary timers or rAF).
- Walk descendants under #root instead of document-wide querySelectorAll('*')
to cut cost on large pages; skip the walk if #root is missing (flag still set).
- Call eval(PAUSE_RENDERING_JS) before hide() on all hide paths (tray menu,
tray icon toggle, mini open/close/show-main, native mini close) so the
webview sees __psyHidden while still fully schedulable.
- Shorten pause_rendering command rustdoc to match the script.
Frontend
- WindowVisibilityProvider: replace self-rescheduling useCallback with a single
effect that keeps one pending timeout id, uses a cancelled flag on unmount,
and syncs hiddenRef from document.hidden at mount (no orphaned timers after
tests/HMR).
- WaveformSeek (preview + animated seekbar): while hidden, poll with a 400 ms
timeout instead of requestAnimationFrame every frame; cancel both rAF and
timeout on teardown.
Why
- Accurate comments prevent future refactors from assuming “full JS idle”.
- Smaller DOM walks and fewer rAF wakeups reduce spikes when hiding the main
window or mini player on Windows WebView2 and elsewhere.
* perf(ui): pause global CSS + timers when Tauri hides the window
Builds on PR #273 (`7803d8e` + `0e07a73`). Adds a second HTML flag driven only by
Rust pause/resume inject so infinite animations still pause when WebView2 keeps
`document.hidden === false` after `win.hide()`, and stops a few periodic JS
timers while the window is invisible.
- `lib.rs`: set/remove `data-psy-native-hidden` on `<html>` in PAUSE/RESUME JS;
document why in rustdoc (rest of pause/resume unchanged from PR tip).
- `components.css`: same `animation-play-state: paused !important` rules as
`data-app-hidden` for `[data-psy-native-hidden="true"]`; refresh the comment.
- `App.tsx`: comment distinguishes tab visibility vs Tauri-native hide.
- `Hero.tsx`: do not run the 10 s carousel interval while hidden
(`useWindowVisibility` + `__psyHidden` in tick).
- `PlaybackScheduleBadge.tsx`, `playbackScheduleFormat.ts`: no 400/500 ms
intervals while hidden; skip ticks when `document.hidden` or `__psyHidden`.
This commit is contained in:
+27
-25
@@ -11,6 +11,7 @@ import PlayerBar from './components/PlayerBar';
|
||||
import BottomNav from './components/BottomNav';
|
||||
import MobilePlayerView from './components/MobilePlayerView';
|
||||
import { useIsMobile } from './hooks/useIsMobile';
|
||||
import { WindowVisibilityProvider } from './hooks/useWindowVisibility';
|
||||
import LiveSearch from './components/LiveSearch';
|
||||
import NowPlayingDropdown from './components/NowPlayingDropdown';
|
||||
import QueuePanel from './components/QueuePanel';
|
||||
@@ -377,11 +378,9 @@ function AppShell() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Pause CSS animations when the window is minimized / hidden.
|
||||
// WebView2 on Windows keeps compositing infinite-loop animations (mesh-aura,
|
||||
// portrait-drift, eq-bounce, …) even when the app is minimized, which shows
|
||||
// up as steady GPU usage. The CSS rule `html[data-app-hidden="true"]` in
|
||||
// components.css pauses all running animations while this flag is set.
|
||||
// Pause CSS animations when the browser tab is hidden (`document.hidden`).
|
||||
// Tauri `win.hide()` is mirrored separately via `data-psy-native-hidden` from
|
||||
// Rust (see components.css). WebView2 can keep compositing without the former.
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
document.documentElement.dataset.appHidden = document.hidden ? 'true' : 'false';
|
||||
@@ -932,10 +931,11 @@ function TauriEventBridge() {
|
||||
unlisten.push(u);
|
||||
}
|
||||
|
||||
// window:close-requested is emitted by Rust (prevent_close + emit).
|
||||
// window:close-requested is emitted by Rust (prevent_close + emit).
|
||||
// JS decides: minimize to tray or exit, based on user setting.
|
||||
const u = await listen('window:close-requested', async () => {
|
||||
if (useAuthStore.getState().minimizeToTray) {
|
||||
await invoke('pause_rendering').catch(() => {});
|
||||
await getCurrentWindow().hide();
|
||||
} else {
|
||||
await invoke('exit_app');
|
||||
@@ -1134,24 +1134,26 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<PasteClipboardHandler />
|
||||
<TauriEventBridge />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<DragDropProvider>
|
||||
<AppShell />
|
||||
</DragDropProvider>
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
{exportPickerOpen && <ExportPickerModal onConfirm={handleExport} onClose={() => setExportPickerOpen(false)} />}
|
||||
<ZipDownloadOverlay />
|
||||
</BrowserRouter>
|
||||
<WindowVisibilityProvider>
|
||||
<BrowserRouter>
|
||||
<PasteClipboardHandler />
|
||||
<TauriEventBridge />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<DragDropProvider>
|
||||
<AppShell />
|
||||
</DragDropProvider>
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
{exportPickerOpen && <ExportPickerModal onConfirm={handleExport} onClose={() => setExportPickerOpen(false)} />}
|
||||
<ZipDownloadOverlay />
|
||||
</BrowserRouter>
|
||||
</WindowVisibilityProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user