Skip to content

fix(desktop): keep renderer running in background + faster balance polling#958

Merged
lefarcen merged 2 commits intomainfrom
fix/desktop-background-throttle-and-balance-poll
Apr 8, 2026
Merged

fix(desktop): keep renderer running in background + faster balance polling#958
lefarcen merged 2 commits intomainfrom
fix/desktop-background-throttle-and-balance-poll

Conversation

@lefarcen
Copy link
Copy Markdown
Collaborator

@lefarcen lefarcen commented Apr 8, 2026

What

Two related fixes that both improve perceived freshness of the desktop client when the user has nexu in the background or returns to it.

Why

1. Setup-animation video freezes when window goes to background

The cold-start setup-animation pauses the moment the user switches to another app, then doesn't resume until they come back. The animation is supposed to play through and then loop infinitely until cold-start finishes — when it freezes mid-animation, the cold-start hand-off looks broken.

Root cause: Chromium suspends background renderers and pauses muted hidden videos to save power. None of the three relevant chromium switches are set in our main process bootstrap. The video element already uses muted + playsInline + loop, so the markup is fine — the suspension happens at the renderer-process level.

2. Bottom-left credit balance feels stale

Multiple colleagues reported that the credit balance shown in the bottom-left of the workspace updates very slowly. Tracing the data flow:

  • Frontend useDesktopRewardsStatus polled the controller every 60 seconds
  • Controller getDesktopRewardsStatus fetches from cloud each call (no cache)
  • Cloud /api/v1/rewards/status returns the current balance

So the only refresh mechanism was the 60s frontend polling. The previous 60s interval was an order of magnitude longer than every other poll in the app:

Data Interval
Bottom-left credit balance 60s ⚠️
Home channels 2s
Home liveStatus 3s
Home sessions 3s
Channels page 3s
Sidebar sessions 10s
Activity feed 30s

On top of that, react-query's refetchIntervalInBackground default is false, so when the window was unfocused the polling stopped entirely — making the displayed balance arbitrarily stale by the time the user came back.

How

1. Three Chromium command-line flags + window-level backup

app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");

Plus on the BrowserWindow:

webPreferences: {
  ...
  backgroundThrottling: false,
}

The flags are applied app-wide; backgroundThrottling: false is the window-level backup. Together they keep the renderer running at full speed regardless of focus state, so the setup-animation video keeps playing when the user alt-tabs away.

2. Per-call refetchInterval with a 60s defensive floor

refetchInterval: () =>
  typeof document !== "undefined" &&
  document.visibilityState === "visible" &&
  document.hasFocus()
    ? 30_000
    : 60_000,
refetchIntervalInBackground: true,
refetchOnWindowFocus: true,
State Interval
visibilityState === "visible" AND document.hasFocus() === true 30s
Anything else (background, occluded, blurred, focus check broken, ...) 60s (defensive floor)
Window regains focus Immediate refetch (refetchOnWindowFocus)

Why a function instead of a number:

  • We want fast polling when the user is actively looking (30s) and slower polling when they're not (60s).
  • A constant refetchInterval can only be one or the other.
  • A function gets re-evaluated every time react-query schedules the next refetch, so the active interval naturally tracks the focus state.

Why a 60s defensive floor instead of "no polling in background":

  • Electron windows can end up in odd states where document.hasFocus() or visibilityState lies (occluded, fullscreen on another space, dock minimised, ...).
  • If the focus detection misfires, we still want the balance to refresh at least once a minute rather than never.
  • refetchIntervalInBackground: true is required for the function to get re-evaluated when backgrounded — without it, react-query stops calling it entirely.

Net effect: the value the user actively sees is always ≤ 30s old, the worst case (focus detection broken) is ≤ 60s, and the moment the user returns to the window it snaps fresh.

Affected areas

  • Desktop app (Electron shell)
  • Web dashboard (React UI)

Checklist

  • pnpm typecheck — not run locally (worktree without node_modules); changes are type-stable: 3 string-literal command-line switches, one boolean field on webPreferences, and a function-form refetchInterval matching react-query's documented signature
  • pnpm lint — not run locally for the same reason
  • pnpm test — N/A
  • No credentials or tokens in code or logs
  • No any types introduced

Notes for reviewers

  • Trade-off on the chromium flags: keeping the renderer at full speed when backgrounded means slightly higher idle CPU when nexu is in the dock. Acceptable because the only animations actively running are the brief startup sequence and the idle bot loop on the home page — both are static after a few seconds.
  • Why 30s and not lower (e.g. 15s or 5s): anything below ~30s starts feeling like spam to the controller and cloud for a value that changes maybe a few times per minute at peak. 30s is 2× faster than before and still in line with the activity-feed interval.
  • Future improvement (not in this PR): push-based balance updates via WebSocket or SSE would eliminate the polling entirely. That's a bigger change touching cloud + controller and is intentionally out of scope here.

lefarcen added 2 commits April 8, 2026 22:11
…lling

Two related fixes that both improve perceived freshness of the desktop
client when the user has nexu in the background or returns to it:

1. Disable Chromium background throttling for the main window.

   The cold-start setup-animation video pauses the moment the user
   switches to another app because Chromium suspends background
   renderers and pauses muted hidden videos to save power. The user
   sees the animation freeze, comes back, and the cold-start hand-off
   looks broken. Adding the three command-line flags
   (disable-background-timer-throttling, disable-renderer-backgrounding,
   disable-backgrounding-occluded-windows) plus webPreferences
   backgroundThrottling: false keeps the renderer at full speed
   regardless of focus state.

2. Reduce the bottom-left credit balance polling interval from 60s
   to 15s, and rely on react-query's default focus refetch when the
   window comes back from background.

   Colleagues reported the balance feels stale and laggy. The previous
   60s interval was an order of magnitude longer than every other poll
   in the app (sessions: 10s, channels: 3s, liveStatus: 3s). 15s + the
   default refetchOnWindowFocus means the value the user actually sees
   is always at most 15s old, with no extra background API traffic.
Switch to a per-call refetchInterval that returns 30s when the window
is visible AND OS-focused, 60s otherwise. The 60s floor is a defensive
backstop in case focus detection misbehaves (Electron windows can end
up in odd states), so the balance still refreshes every minute no
matter what. refetchIntervalInBackground: true is required to make
the function fire while backgrounded.
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 33.33333% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/desktop/main/index.ts 0.00% 4 Missing ⚠️
apps/web/src/hooks/use-desktop-rewards.ts 50.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lefarcen lefarcen merged commit 140831c into main Apr 8, 2026
15 checks passed
@lefarcen lefarcen mentioned this pull request Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants