From c0688e0dc44feec8e6cea065218f32c4531734f8 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 06:00:34 +0100 Subject: [PATCH] perf: optimize WebUI rendering and CSS performance Frontend JS: - Add render debouncing for renderInnerStatePanel (50ms) - Add render debouncing for renderTaskPanel (100ms) - Add render debouncing for renderScreenSessions (100ms) - Store timeout references for cleanup CSS Performance: - Add CSS containment to terminal container (strict) - Add containment to session tabs (layout) - Add containment to ralph panel (layout style paint) - Add containment to task panel (layout style) - Add containment to modal content (layout style paint) - Add will-change for animated elements - Use GPU-accelerated transitions where appropriate These optimizations reduce layout thrashing and isolate paint operations for smoother 60fps rendering. Co-Authored-By: Claude Opus 4.5 --- src/web/public/app.js | 33 +++++++++++++++++++++++++++++++++ src/web/public/styles.css | 22 ++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index e133e5c4..38c379e1 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -29,6 +29,9 @@ class ClaudemanApp { // Render debouncing this.renderSessionTabsTimeout = null; + this.renderInnerStatePanelTimeout = null; + this.renderTaskPanelTimeout = null; + this.renderScreenSessionsTimeout = null; // System stats polling this.systemStatsInterval = null; @@ -1912,6 +1915,16 @@ class ClaudemanApp { } renderTaskPanel() { + // Debounce renders at 100ms to prevent excessive DOM updates + if (this.renderTaskPanelTimeout) { + clearTimeout(this.renderTaskPanelTimeout); + } + this.renderTaskPanelTimeout = setTimeout(() => { + this._renderTaskPanelImmediate(); + }, 100); + } + + _renderTaskPanelImmediate() { const session = this.sessions.get(this.activeSessionId); const body = document.getElementById('backgroundTasksBody'); const stats = document.getElementById('taskPanelStats'); @@ -2128,6 +2141,16 @@ class ClaudemanApp { } renderInnerStatePanel() { + // Debounce renders at 50ms to prevent excessive DOM updates + if (this.renderInnerStatePanelTimeout) { + clearTimeout(this.renderInnerStatePanelTimeout); + } + this.renderInnerStatePanelTimeout = setTimeout(() => { + this._renderInnerStatePanelImmediate(); + }, 50); + } + + _renderInnerStatePanelImmediate() { const panel = this.$('innerStatePanel'); const toggle = this.$('ralphToggle'); @@ -2462,6 +2485,16 @@ class ClaudemanApp { } renderScreenSessions() { + // Debounce renders at 100ms to prevent excessive DOM updates + if (this.renderScreenSessionsTimeout) { + clearTimeout(this.renderScreenSessionsTimeout); + } + this.renderScreenSessionsTimeout = setTimeout(() => { + this._renderScreenSessionsImmediate(); + }, 100); + } + + _renderScreenSessionsImmediate() { const body = document.getElementById('screenSessionsBody'); if (!this.screenSessions || this.screenSessions.length === 0) { diff --git a/src/web/public/styles.css b/src/web/public/styles.css index ed50b2e2..68d1cfbf 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -77,6 +77,8 @@ body { overflow-y: auto; max-height: 120px; padding: 0.15rem 0.25rem; + /* Performance: isolate layout recalculations */ + contain: layout; } .session-tabs::-webkit-scrollbar { @@ -134,7 +136,12 @@ body { } .session-tab .tab-status.idle { background: var(--green); } -.session-tab .tab-status.busy { background: var(--green); animation: pulse 1.5s infinite; } +.session-tab .tab-status.busy { + background: var(--green); + animation: pulse 1.5s infinite; + /* Performance: GPU acceleration for animation */ + will-change: opacity; +} .session-tab .tab-status.error { background: var(--red); } .session-tab .tab-name { @@ -150,7 +157,8 @@ body { line-height: 1; color: var(--text-muted); overflow: hidden; - transition: opacity 0.15s, width 0.15s, padding 0.15s; + /* Performance: use transform for GPU acceleration */ + transition: opacity 0.15s ease-out, width 0.15s ease-out, padding 0.15s ease-out; } .session-tab:hover .tab-close { @@ -491,6 +499,8 @@ body { overflow: hidden; position: relative; overflow-anchor: none; /* Prevent scroll anchoring when expanding/collapsing */ + /* Performance: isolate paint and layout operations */ + contain: layout style paint; } /* Mini Progress Ring (in summary) */ @@ -1066,6 +1076,9 @@ body { flex: 1; background: #0d0d0d; overflow: hidden; + /* Performance: isolate layout and paint operations */ + contain: strict; + will-change: contents; } .terminal-container .xterm { @@ -1707,6 +1720,8 @@ body { width: 90%; max-width: 400px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + /* Performance: isolate modal paint operations */ + contain: layout style paint; } .modal-content.modal-sm { @@ -2145,6 +2160,9 @@ kbd { right: 0.5rem; width: 350px; max-width: calc(100vw - 1rem); + /* Performance: GPU-accelerated transforms and isolated paint */ + contain: layout style; + will-change: transform; max-height: 50vh; background: var(--bg-card); border: 1px solid var(--border);