perf(renderer): coalesce status-change UI updates into one rAF frame

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.
This commit is contained in:
Administrator 2026-04-21 19:33:33 +02:00
parent 4bf159eda2
commit 058c8a2674

View File

@ -918,6 +918,23 @@ function scheduleThrottledUIUpdate() {
}, UI_UPDATE_INTERVAL); }, 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) { function buildRowHtml(job) {
const statusClass = `status-${job.status}`; const statusClass = `status-${job.status}`;
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`; const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
@ -1859,14 +1876,13 @@ function handleProgress(data) {
queueJobs = queueJobs.filter(j => j !== job); 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') { if (data.status === 'uploading') {
scheduleThrottledUIUpdate(); scheduleThrottledUIUpdate();
} else { } else {
scheduleQueueRender(); scheduleStatusChangeUpdate();
updateQueueActionButtons();
updateStatusBar();
updateStatsPanel();
} }
persistQueueStateSoon(); persistQueueStateSoon();
} }