From 52fb96de4b4a8fa48dfa19d566a8bf14a6b26bf4 Mon Sep 17 00:00:00 2001 From: Daisuke Suzuki Date: Mon, 3 Aug 2026 17:47:16 +0530 Subject: [PATCH] perf(desktop): pause hidden-pane timers in agents view, cron sidebar, and floating pet Partial pick of the surviving renderer hunks from #75395 (perf commit 6502e441d plus fixup 3fbbc9c1d): gate the 500ms subagent now-ticker and the cron sidebar 1s ticker/run-poll on usePaneVisible, and skip the legacy floating-pet poll while the document is hidden. Dropped hunks (electron/main.ts, vitest.setup.ts/config) intentionally excluded. --- apps/desktop/src/app/agents/index.tsx | 7 +++++-- .../app/chat/sidebar/cron-jobs-section.tsx | 20 +++++++++++++------ .../src/components/pet/floating-pet.tsx | 6 +++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx index fe392e84610cc..8df8566a80737 100644 --- a/apps/desktop/src/app/agents/index.tsx +++ b/apps/desktop/src/app/agents/index.tsx @@ -1,6 +1,7 @@ import { useStore } from '@nanostores/react' import { type ReactNode, useEffect, useMemo, useState } from 'react' +import { usePaneVisible } from '@/components/pane-shell/pane-visibility' import { useElapsedSeconds } from '@/components/chat/activity-timer' import { ActivityTimerText } from '@/components/chat/activity-timer-text' import { Codicon } from '@/components/ui/codicon' @@ -189,15 +190,17 @@ function SubagentTree({ tree }: { tree: SubagentNode[] }) { const tokens = flat.reduce((sum, n) => sum + (n.inputTokens ?? 0) + (n.outputTokens ?? 0), 0) const cost = flat.reduce((sum, n) => sum + (n.costUsd ?? 0), 0) + const visible = usePaneVisible() + useEffect(() => { - if (active <= 0 || typeof window === 'undefined') { + if (active <= 0 || !visible || typeof window === 'undefined') { return } const id = window.setInterval(() => setNowMs(Date.now()), 500) return () => window.clearInterval(id) - }, [active]) + }, [active, visible]) if (tree.length === 0) { return ( diff --git a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx index dd6988ce9165e..67a429ecc80fb 100644 --- a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +++ b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx @@ -1,6 +1,8 @@ import { useStore } from '@nanostores/react' import { useEffect, useMemo, useState } from 'react' +import { usePaneVisible } from '@/components/pane-shell/pane-visibility' + import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu' import { Codicon } from '@/components/ui/codicon' import { DisclosureCaret } from '@/components/ui/disclosure-caret' @@ -92,17 +94,19 @@ export function SidebarCronJobsSection({ // Rows revealed so far; starts compact, grows in steps via "load more". const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_JOBS) + const visible = usePaneVisible() + // One clock for the whole section (rows are pure) so the countdowns tick - // without re-rendering the rest of the sidebar. Only runs while expanded. + // without re-rendering the rest of the sidebar. Only runs while expanded and visible. useEffect(() => { - if (!open) { + if (!open || !visible) { return } const id = window.setInterval(() => setNowMs(Date.now()), 1000) return () => window.clearInterval(id) - }, [open]) + }, [open, visible]) // Upcoming first (soonest next run), jobs with no next run sink to the bottom, // then alphabetical for stability. @@ -328,6 +332,7 @@ function CronJobSidebarRuns({ jobId, onOpenRun }: { jobId: string; onOpenRun: (s const changeEventsAvailable = useStore($changeEventsAvailable) const cronChangeTick = useStore($cronChangeTick) const [runs, setRuns] = useState(null) + const visible = usePaneVisible() useEffect(() => { let cancelled = false @@ -345,11 +350,14 @@ function CronJobSidebarRuns({ jobId, onOpenRun }: { jobId: string; onOpenRun: (s } }) - void load() + // Initial load when visible + if (visible) { + void load() + } const intervalId = window.setInterval( () => { - if (document.visibilityState === 'visible') { + if (visible && document.visibilityState === 'visible') { void load() } }, @@ -361,7 +369,7 @@ function CronJobSidebarRuns({ jobId, onOpenRun }: { jobId: string; onOpenRun: (s window.clearInterval(intervalId) } // cronChangeTick: a fired run reloads the peek immediately. - }, [changeEventsAvailable, cronChangeTick, jobId]) + }, [changeEventsAvailable, cronChangeTick, jobId, visible]) return (
diff --git a/apps/desktop/src/components/pet/floating-pet.tsx b/apps/desktop/src/components/pet/floating-pet.tsx index 2b7dff0d4ce92..3e800e8ff1e7f 100644 --- a/apps/desktop/src/components/pet/floating-pet.tsx +++ b/apps/desktop/src/components/pet/floating-pet.tsx @@ -214,7 +214,11 @@ export function FloatingPet() { // so no timer. Legacy backend: the historical poll. const timer = changeEventsAvailable ? null - : window.setInterval(() => void pull(), active ? PET_ACTIVE_REFRESH_MS : PET_POLL_MS) + : window.setInterval(() => { + if (document.visibilityState === 'visible') { + void pull() + } + }, active ? PET_ACTIVE_REFRESH_MS : PET_POLL_MS) return () => { cancelled = true