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(); }