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:
Ivan Pelipenko
2026-04-23 23:47:23 +03:00
committed by GitHub
parent 694567843f
commit 0ead4fbeb1
9 changed files with 258 additions and 52 deletions
+60
View File
@@ -0,0 +1,60 @@
import {
createContext,
useContext,
useState,
useRef,
useEffect,
type ReactNode,
} from 'react';
const WindowVisibilityContext = createContext(false);
/**
* Tracks whether the Tauri window is hidden.
*
* On Windows WebView2, `visibilitychange` and `blur`/`focus` events do not
* fire when `win.hide()` is called. We fall back to polling `document.hidden`
* with an adaptive interval: fast checks while visible (to catch show quickly),
* slow checks while hidden (to minimize CPU wakeups).
*/
export function WindowVisibilityProvider({ children }: { children: ReactNode }) {
const [hidden, setHidden] = useState(document.hidden);
const hiddenRef = useRef(hidden);
useEffect(() => {
hiddenRef.current = document.hidden;
let cancelled = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const schedule = () => {
if (cancelled) return;
const interval = hiddenRef.current ? 1000 : 200;
timeoutId = setTimeout(() => {
timeoutId = null;
if (cancelled) return;
const current = document.hidden;
if (current !== hiddenRef.current) {
hiddenRef.current = current;
setHidden(current);
}
schedule();
}, interval);
};
schedule();
return () => {
cancelled = true;
if (timeoutId !== null) clearTimeout(timeoutId);
};
}, []);
return (
<WindowVisibilityContext.Provider value={hidden}>
{children}
</WindowVisibilityContext.Provider>
);
}
export function useWindowVisibility() {
return useContext(WindowVisibilityContext);
}