diff --git a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx index 85025688..226bba8a 100644 --- a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx +++ b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx @@ -405,6 +405,10 @@ export function DesktopHostSwitcherDialog({ }; void run(); const interval = window.setInterval(() => { + // Skip polling when tab is hidden to reduce background work + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return; + } void run(); }, 1_500); @@ -1255,6 +1259,10 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost void run(); const interval = window.setInterval(() => { + // Skip polling when tab is hidden to reduce background work + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return; + } void run(); }, 10_000); return () => { diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index d8d7175c..d4ebdbae 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -250,12 +250,41 @@ export const OpenChamberVisualSettings: React.FC return; } - const intervalId = setInterval(() => { - setChatRenderPreviewTick((prev) => (prev + 1) % 24); - }, 420); + // Use requestAnimationFrame for smoother animation without setInterval overhead + let rafId: number | null = null; + let lastTime = Date.now(); + + const tick = () => { + const now = Date.now(); + // Update every ~420ms + if (now - lastTime >= 420) { + setChatRenderPreviewTick((prev) => (prev + 1) % 24); + lastTime = now; + } + rafId = requestAnimationFrame(tick); + }; + + // Only run when visible + if (typeof document === 'undefined' || document.visibilityState === 'visible') { + rafId = requestAnimationFrame(tick); + } + + const onVisibility = () => { + if (document.visibilityState === 'visible' && rafId === null) { + rafId = requestAnimationFrame(tick); + } else if (document.visibilityState !== 'visible' && rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + + document.addEventListener('visibilitychange', onVisibility); return () => { - clearInterval(intervalId); + document.removeEventListener('visibilitychange', onVisibility); + if (rafId !== null) { + cancelAnimationFrame(rafId); + } }; }, [shouldAnimateChatPreview]); diff --git a/packages/ui/src/components/sections/openchamber/TunnelSettings.tsx b/packages/ui/src/components/sections/openchamber/TunnelSettings.tsx index e3702240..323fa3b0 100644 --- a/packages/ui/src/components/sections/openchamber/TunnelSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/TunnelSettings.tsx @@ -486,6 +486,9 @@ export const TunnelSettings: React.FC = () => { return; } + let rafId: number | null = null; + let lastTime = Date.now(); + const updateRemaining = () => { const remaining = tunnelInfo.bootstrapExpiresAt ? tunnelInfo.bootstrapExpiresAt - Date.now() : 0; if (remaining <= 0) { @@ -495,14 +498,79 @@ export const TunnelSettings: React.FC = () => { } }; + const tick = () => { + const now = Date.now(); + // Update only once per second + if (now - lastTime >= 1_000) { + updateRemaining(); + lastTime = now; + } + rafId = requestAnimationFrame(tick); + }; + updateRemaining(); - const timer = window.setInterval(updateRemaining, 1000); - return () => window.clearInterval(timer); + + // Only run when visible + if (typeof document === 'undefined' || document.visibilityState === 'visible') { + rafId = requestAnimationFrame(tick); + } + + const onVisibility = () => { + if (document.visibilityState === 'visible' && rafId === null) { + rafId = requestAnimationFrame(tick); + } else if (document.visibilityState !== 'visible' && rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + + document.addEventListener('visibilitychange', onVisibility); + + return () => { + document.removeEventListener('visibilitychange', onVisibility); + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + }; }, [tunnelInfo?.bootstrapExpiresAt]); React.useEffect(() => { - const timer = window.setInterval(() => setNowTs(Date.now()), 1000); - return () => window.clearInterval(timer); + // Use requestAnimationFrame for smoother updates without setInterval overhead + let rafId: number | null = null; + let lastTime = Date.now(); + + const tick = () => { + const now = Date.now(); + // Update only once per second + if (now - lastTime >= 1_000) { + setNowTs(now); + lastTime = now; + } + rafId = requestAnimationFrame(tick); + }; + + // Only run when visible + if (typeof document === 'undefined' || document.visibilityState === 'visible') { + rafId = requestAnimationFrame(tick); + } + + const onVisibility = () => { + if (document.visibilityState === 'visible' && rafId === null) { + rafId = requestAnimationFrame(tick); + } else if (document.visibilityState !== 'visible' && rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + + document.addEventListener('visibilitychange', onVisibility); + + return () => { + document.removeEventListener('visibilitychange', onVisibility); + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + }; }, []); React.useEffect(() => { @@ -530,6 +598,10 @@ export const TunnelSettings: React.FC = () => { }; const timer = window.setInterval(() => { + // Skip polling when tab is hidden + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return; + } void refreshSessions(); }, 5000); diff --git a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx index 36e27ec0..a12117cf 100644 --- a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx +++ b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx @@ -305,6 +305,10 @@ export const RemoteInstancesPage: React.FC = () => { return; } const interval = window.setInterval(() => { + // Skip polling when tab is hidden to reduce background work + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return; + } void refreshStatuses(); }, 2_000); return () => { @@ -313,11 +317,41 @@ export const RemoteInstancesPage: React.FC = () => { }, [refreshStatuses, selectedId]); React.useEffect(() => { - const interval = window.setInterval(() => { - setClockMs(Date.now()); - }, 1_000); + // Use requestAnimationFrame for smoother clock updates without setInterval overhead + let rafId: number | null = null; + let lastTime = Date.now(); + + const tick = () => { + const now = Date.now(); + // Update only once per second + if (now - lastTime >= 1_000) { + setClockMs(now); + lastTime = now; + } + rafId = requestAnimationFrame(tick); + }; + + // Only run when visible + if (typeof document === 'undefined' || document.visibilityState === 'visible') { + rafId = requestAnimationFrame(tick); + } + + const onVisibility = () => { + if (document.visibilityState === 'visible' && rafId === null) { + rafId = requestAnimationFrame(tick); + } else if (document.visibilityState !== 'visible' && rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + + document.addEventListener('visibilitychange', onVisibility); + return () => { - window.clearInterval(interval); + document.removeEventListener('visibilitychange', onVisibility); + if (rafId !== null) { + cancelAnimationFrame(rafId); + } }; }, []); @@ -525,6 +559,10 @@ export const RemoteInstancesPage: React.FC = () => { void run(); const interval = window.setInterval(() => { + // Skip polling when tab is hidden + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return; + } void run(); }, 1_000);