perf(launcher): reduce background resource usage

Make the custom cursor event-driven, pause CSS, theme video, and Lottie work
while the launcher is hidden or unfocused, and reduce presence polling while
osu is running.
This commit is contained in:
HorizonCode committed 2026-11-04 17:00:16 +01:00
1 parent 687ab26752
commit c2fd265b49
8 files changed
+171 -31

No files matched your search

+7
View File
@@ -174,6 +174,13 @@
}
}
/* Pause continuous visual work while the launcher is hidden or in the background. */
main.app-inactive *,
main.app-inactive *::before,
main.app-inactive *::after {
animation-play-state: paused !important;
}
/* Suppress browser and WebKit native media controls and overlay play buttons on theme videos */
video::-webkit-media-controls,
video::-webkit-media-controls-enclosure,
@@ -17,12 +17,38 @@
let container = $state<HTMLDivElement | undefined>(undefined);
let animation: AnimationItem | undefined;
let reducedMotion = false;
let windowActive = true;
function syncPlayback() {
if (!animation) return;
animation.loop = loop;
animation.setSpeed(speed);
if (autoplay && !reducedMotion && windowActive && document.visibilityState === 'visible') {
animation.play();
} else {
animation.pause();
}
}
onMount(() => {
if (!container || !src) return;
reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
let cancelled = false;
const onBlur = () => {
windowActive = false;
syncPlayback();
};
const onFocus = () => {
windowActive = true;
syncPlayback();
};
const onVisibilityChange = () => syncPlayback();
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
document.addEventListener('visibilitychange', onVisibilityChange);
const start = (animationData?: unknown) => {
if (cancelled || !container) return;
try {
@@ -30,7 +56,8 @@
container,
renderer: 'svg',
loop,
autoplay: autoplay && !reducedMotion,
autoplay:
autoplay && !reducedMotion && windowActive && document.visibilityState === 'visible',
...(animationData ? { animationData } : { path: src }),
rendererSettings: { preserveAspectRatio: 'xMidYMid meet' },
});
@@ -52,15 +79,14 @@
return () => {
cancelled = true;
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
document.removeEventListener('visibilitychange', onVisibilityChange);
};
});
$effect(() => {
if (!animation) return;
animation.loop = loop;
animation.setSpeed(speed);
if (autoplay && !reducedMotion) animation.play();
else animation.pause();
syncPlayback();
});
onDestroy(() => {
@@ -48,6 +48,7 @@
opacity:{$reduceAnimations ? 0 : s.opacity};
animation-duration:{s.duration}s;
animation-delay:-{s.delay}s;
animation-play-state:{$reduceAnimations ? 'paused' : 'running'};
"
></div>
{/each}
@@ -34,6 +34,7 @@
let velocityAccumX = 0;
let velocityAccumY = 0;
let rotateSpringAnim: ReturnType<typeof animate> | null = null;
let scheduleCursorFrame = () => {};
let cursor: HTMLElement;
let cursorRotateWrapper: HTMLDivElement;
@@ -41,20 +42,15 @@
let cursorAdditive: HTMLImageElement;
function isInteractive(el: Element | null): boolean {
while (el) {
const tag = el.tagName.toLowerCase();
const role = el.getAttribute('role');
const computed = getComputedStyle(el);
if (
['button', 'a', 'input', 'select', 'textarea', 'label', 'option'].includes(tag) ||
(role && ['button', 'link', 'checkbox', 'combobox', 'menuitem', 'option'].includes(role)) ||
computed.cursor === 'pointer'
) {
return true;
}
el = el.parentElement;
if (!el) return false;
if (
el.closest(
'button, a, input, select, textarea, label, option, [role="button"], [role="link"], [role="checkbox"], [role="combobox"], [role="menuitem"], [role="option"]'
)
) {
return true;
}
return false;
return getComputedStyle(el).cursor === 'pointer';
}
function dampContinuously(current: number, target: number, halfLife: number, dt: number): number {
@@ -133,6 +129,7 @@
}
}
scheduleCursorFrame();
lastMouseX = targetX;
lastMouseY = targetY;
};
@@ -147,6 +144,7 @@
lastVelocityTime = performance.now();
isMouseDown = true;
scheduleCursorFrame();
if (rotateSpringAnim) {
rotateSpringAnim.pause();
@@ -213,12 +211,17 @@
document.addEventListener('mouseleave', handleMouseLeave);
document.addEventListener('mouseenter', handleMouseEnter);
let animationFrameId: number;
let animationFrameId: number | undefined;
let lastFrameTime = performance.now();
let paused = false;
const tick = (now: number) => {
animationFrameId = undefined;
if (paused) return;
const dt = Math.min(Math.max(now - lastFrameTime, 0), 64);
lastFrameTime = now;
let needsAnotherFrame = false;
if (hasMoved && cursor) {
if (!smoothCursor) {
@@ -229,6 +232,8 @@
const halfLife = $cursorSmoothness > 50 ? 14 : Math.max(1, $cursorSmoothness || 14);
currentX = dampContinuously(currentX, targetX, halfLife, dt);
currentY = dampContinuously(currentY, targetY, halfLife, dt);
needsAnotherFrame =
Math.abs(targetX - currentX) >= 0.05 || Math.abs(targetY - currentY) >= 0.05;
}
if (Math.abs(targetX - currentX) < 0.05) currentX = targetX;
@@ -244,19 +249,62 @@
const rotFactor = 1 - Math.pow(0.5, dt / 25);
currentDegrees += (targetDegrees - currentDegrees) * rotFactor;
cursorRotateWrapper.style.transform = `rotate(${currentDegrees}deg)`;
needsAnotherFrame = true;
}
}
if (needsAnotherFrame) {
animationFrameId = requestAnimationFrame(tick);
}
};
scheduleCursorFrame = () => {
if (paused || animationFrameId !== undefined) return;
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(tick);
};
animationFrameId = requestAnimationFrame(tick);
const suspendCursor = () => {
paused = true;
if (animationFrameId !== undefined) {
cancelAnimationFrame(animationFrameId);
animationFrameId = undefined;
}
if (cursor) cursor.style.opacity = '0';
};
const resumeCursor = () => {
if (!paused) return;
paused = false;
lastFrameTime = performance.now();
if (!hasMoved) return;
if (cursor) cursor.style.opacity = '1';
scheduleCursorFrame();
};
const onVisibilityChange = () => {
if (document.visibilityState === 'hidden') suspendCursor();
else if (document.hasFocus()) resumeCursor();
};
const onWindowBlur = () => suspendCursor();
const onWindowFocus = () => {
if (document.visibilityState !== 'hidden') resumeCursor();
};
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('blur', onWindowBlur);
window.addEventListener('focus', onWindowFocus);
onVisibilityChange();
return () => {
document.documentElement.classList.remove('hiddenCursor');
document.removeEventListener('mouseleave', handleMouseLeave);
document.removeEventListener('mouseenter', handleMouseEnter);
cancelAnimationFrame(animationFrameId);
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('blur', onWindowBlur);
window.removeEventListener('focus', onWindowFocus);
scheduleCursorFrame = () => {};
suspendCursor();
if (rotateSpringAnim) {
rotateSpringAnim.pause();
}
+19 -4
View File
@@ -532,12 +532,13 @@ export function themeMediaSuspension(node: HTMLElement) {
/**
* Manage background video playback across document visibility changes.
* Videos continue playing when the launcher window loses focus. Muted background
* videos pause only when the document is hidden/minimized to save GPU decoding
* cycles without interrupting theme music or audio.
* Muted background videos pause when the document is hidden/minimized or the
* launcher loses focus, which saves GPU decoding cycles behind a running game
* without interrupting unmuted theme music or audio.
*/
export function themeVideoLifecycle(node: HTMLVideoElement) {
let pausedByLifecycle = false;
let focused = true;
const pause = () => {
if (node.paused) return;
@@ -551,12 +552,22 @@ export function themeVideoLifecycle(node: HTMLVideoElement) {
playThemeMedia(node);
};
const onBlur = () => {
focused = false;
sync();
};
const onFocus = () => {
focused = true;
sync();
};
const sync = () => {
if (!node.isConnected) {
document.removeEventListener('visibilitychange', sync);
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
return;
}
if (document.visibilityState === 'visible') {
if (document.visibilityState === 'visible' && focused) {
resume();
} else if (node.muted || node.volume === 0) {
pause();
@@ -564,11 +575,15 @@ export function themeVideoLifecycle(node: HTMLVideoElement) {
};
document.addEventListener('visibilitychange', sync);
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
sync();
return {
destroy() {
document.removeEventListener('visibilitychange', sync);
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
},
};
}
+1 -1
View File
@@ -133,7 +133,7 @@ export const urlIsValidImage = async (url: string) => {
const request = await fetch(url);
if (!request.ok) return false;
const contentType = request.headers.get('content-type');
return contentType?.startsWith('image/');
return contentType?.startsWith('image/') === true;
} catch {
return false;
}
+32
View File
@@ -71,6 +71,37 @@
let { children } = $props();
function appActivity(node: HTMLElement) {
let focused = true;
const sync = () => {
node.classList.toggle('app-inactive', document.hidden || !focused);
};
const onBlur = () => {
focused = false;
sync();
};
const onFocus = () => {
focused = true;
sync();
};
const onVisibilityChange = () => sync();
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
document.addEventListener('visibilitychange', onVisibilityChange);
sync();
return {
destroy() {
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
document.removeEventListener('visibilitychange', onVisibilityChange);
node.classList.remove('app-inactive');
},
};
}
let unlistenEditorState: (() => void) | undefined;
let unlistenRequestEditorData: (() => void) | undefined;
let unlistenPresenceEditorState: (() => void) | undefined;
@@ -552,6 +583,7 @@
</AlertDialog.Root>
<main
use:appActivity
use:themeMediaSuspension
use:themeParallax={{
enabled: $active_custom_theme?.definition?.parallax?.enabled === true,
+15 -4
View File
@@ -1580,6 +1580,7 @@
await getCurrentWindow().hide();
let presenceUpdater: number | undefined = undefined;
const beatmapCoverValidity = new SvelteMap<string, boolean>();
const isPresenceConnected = await presence.isConnected();
@@ -1592,8 +1593,10 @@
return;
}
if ($currentUser) {
const userStats = await ezppfarm.getUserInfo($currentUser.id, 'stats');
const userStatus = await ezppfarm.getUserStatus($currentUser.id);
const [userStats, userStatus] = await Promise.all([
ezppfarm.getUserInfo($currentUser.id, 'stats'),
ezppfarm.getUserStatus($currentUser.id),
]);
if (userStatus?.player_status.online) {
let actionName = 'Idle...';
const gamemode = getModeAndTypeFromGamemode(userStatus.player_status.status.mode);
@@ -1648,7 +1651,15 @@
if (beatmap !== null) {
beatmapString = `${beatmap.artist} - ${beatmap.title} [${beatmap.version}]`;
const coverCandidate = `https://assets.ppy.sh/beatmaps/${beatmap.set_id}/covers/[email protected]`;
const isValidImage = await urlIsValidImage(coverCandidate);
const cachedCoverValidity = beatmapCoverValidity.get(coverCandidate);
let isValidImage: boolean;
if (cachedCoverValidity === undefined) {
isValidImage = await urlIsValidImage(coverCandidate);
if (beatmapCoverValidity.size >= 32) beatmapCoverValidity.clear();
beatmapCoverValidity.set(coverCandidate, isValidImage);
} else {
isValidImage = cachedCoverValidity;
}
if (isValidImage) beatmapCoverImage = coverCandidate;
}
@@ -1678,7 +1689,7 @@
}
}
}
}, 1000 * 2);
}, 5000);
}
await runOsu(osuPath, $patch, $gameOptimization);