From 058c8a26747d4aeb0277ae9ddebc6d67c65def51 Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 21 Apr 2026 19:33:33 +0200 Subject: [PATCH] perf(renderer): coalesce status-change UI updates into one rAF frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-uploading progress events (queued/getting-server/retrying/done/ error/aborted/skipped) were firing renderQueueTable + updateQueueActionButtons + updateStatusBar + updateStatsPanel synchronously on EVERY event. At batch start, 500 jobs going preview→queued→getting-server within milliseconds meant ~2000 sync DOM updates — visible jank on large batches. New scheduleStatusChangeUpdate() uses requestAnimationFrame to coalesce the four-helper call into at most one run per frame (~60 Hz). Functional result is identical; the user just sees smooth flips instead of a briefly frozen renderer. The uploading-progress throttle (200ms) is unchanged since those events are much more frequent and the user doesn't need 60 Hz upload-byte updates. --- renderer/app.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index b1338be..4b41589 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -918,6 +918,23 @@ function scheduleThrottledUIUpdate() { }, UI_UPDATE_INTERVAL); } +// Coalesces status-change updates (done/error/retrying/queued/…) into one +// frame. Without this, a batch of 500 jobs flipping queued→getting-server +// →uploading synchronously fires 1500+ updateStatusBar/Buttons/Stats calls +// and janks the renderer. rAF caps it to ~60 Hz. +let _statusChangeUpdateQueued = false; +function scheduleStatusChangeUpdate() { + if (_statusChangeUpdateQueued) return; + _statusChangeUpdateQueued = true; + requestAnimationFrame(() => { + _statusChangeUpdateQueued = false; + renderQueueTable(); + updateQueueActionButtons(); + updateStatusBar(); + updateStatsPanel(); + }); +} + function buildRowHtml(job) { const statusClass = `status-${job.status}`; const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`; @@ -1859,14 +1876,13 @@ function handleProgress(data) { queueJobs = queueJobs.filter(j => j !== job); } - // Status changes (done/error/etc) get immediate render; ongoing progress is throttled + // Status changes (done/error/etc) get one coalesced update per frame so a + // burst of 500 parallel jobs flipping state doesn't fire 2000 sync DOM + // updates. Ongoing uploading progress is throttled at 200ms. if (data.status === 'uploading') { scheduleThrottledUIUpdate(); } else { - scheduleQueueRender(); - updateQueueActionButtons(); - updateStatusBar(); - updateStatsPanel(); + scheduleStatusChangeUpdate(); } persistQueueStateSoon(); }