aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-08 08:19:32 -0700
committerAdam Soltys <[email protected]>2026-07-08 08:19:32 -0700
commit0fb4b03b48bda2ba187d96d1feb4459716b8e73c (patch)
treefaaba06a5b9f5591bb30c4aba334b07db6766a27 /src
parent312621af84136fddbe7d7e47ffecc1ba2b38920d (diff)
downloadcoinosv3-0fb4b03b48bda2ba187d96d1feb4459716b8e73c.tar.gz
coinosv3-0fb4b03b48bda2ba187d96d1feb4459716b8e73c.zip
Coalesce background renders so user input takes priority
Every wallet.emit() re-rendered the whole screen synchronously. On refocus after the PWA was backgrounded, a burst of emits (WS reconnect, the wake scan's several emits, syncFromNostr merge, each live device snapshot) fired a dozen full-screen rebuilds back-to-back, starving input — navigation lagged ~2s until the burst drained. Route the subscription through scheduleRender(), which coalesces emits into at most one render per animation frame. Direct user actions still call render() synchronously and cancel any pending frame, so a tap or navigation always renders immediately and takes priority over background repaints. rAF also skips rendering while hidden and fires once on refocus. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js20
1 files changed, 19 insertions, 1 deletions
diff --git a/src/app.js b/src/app.js
index 547cc65..b0a5fec 100644
--- a/src/app.js
+++ b/src/app.js
@@ -274,6 +274,9 @@ function nodeAtPath(path) {
}
function render() {
+ // A direct render subsumes any coalesced one queued by a background emit, so
+ // drop the pending frame (a user action must not trigger a second rebuild).
+ if (_renderRaf) { cancelAnimationFrame(_renderRaf); _renderRaf = 0; }
// Preserve focus + caret across the rebuild, so a background update (poll, a
// payment push, an SP scan) can't kick the user out of a field they're editing.
const a = document.activeElement;
@@ -312,7 +315,22 @@ function render() {
}
syncHistory(); // mirror the current screen into browser history (Back/Forward)
}
-wallet.subscribe(render);
+
+// Background state changes (a wake scan, cross-device sync merges, WS payment
+// pushes) can fire a burst of emits — especially when the PWA is refocused
+// after being backgrounded. Rendering synchronously on each one rebuilds the
+// whole screen dozens of times in a row and starves user input, so the app
+// feels frozen for a second or two. Coalesce those into at most one render per
+// animation frame. Direct user actions still call render() synchronously, and
+// the browser dispatches input events ahead of the queued frame, so a tap or
+// navigation always takes priority over background repaints. (rAF also skips
+// rendering entirely while the tab is hidden, and fires once on refocus.)
+let _renderRaf = 0;
+function scheduleRender() {
+ if (_renderRaf) return;
+ _renderRaf = requestAnimationFrame(() => { _renderRaf = 0; render(); });
+}
+wallet.subscribe(scheduleRender);
// ---- browser-history navigation ------------------------------------------
// Mirror the app's screen position into the browser history so the Back/Forward