From f16dd9ffa67c48d3840cbe2b93757588ee1349e7 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 19 Apr 2026 13:55:37 +0200 Subject: [PATCH] perf: lazy history refresh + append-only recent panel + queue-cleanup merge Three more targeted wins: - loadHistory() was called unconditionally on every handleBatchDone, doing an IPC roundtrip + full history-table rebuild even when the user is on the Upload tab and can't see it. Now it sets a dirty flag and the actual refresh is deferred until the user switches to the Verlauf tab. On a fresh tab click it always runs. - renderRecentUploadsPanel append-only fast path: when the sort is 'date desc' (the default) and the dataset only grew, the panel inserts the new rows at the top via insertAdjacentHTML instead of rebuilding the 5000-row tbody from scratch. Length shrinks or sort-change still trigger a full rebuild. - handleBatchDone's removeFromQueueOnDone cleanup now does one pass (build keep-list + detach from index together) instead of two separate filter() scans over queueJobs. --- renderer/app.js | 73 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index e5cd0fc..581c8af 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -200,13 +200,21 @@ async function init() { } // --- Tab switching --- +let _historyDirty = false; +function _isHistoryTabActive() { + const tab = document.querySelector('.tab.active'); + return !!(tab && tab.dataset.view === 'history'); +} document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); tab.classList.add('active'); document.getElementById(`${tab.dataset.view}-view`).classList.add('active'); - if (tab.dataset.view === 'history') loadHistory(); + if (tab.dataset.view === 'history') { + _historyDirty = false; + loadHistory(); + } }); }); @@ -1820,16 +1828,25 @@ function handleBatchDone(summary) { updateQueueActionButtons(); renderQueueTable(); renderRecentUploadsPanel(); - loadHistory(); + // History is only visible on the Verlauf tab. Mark it dirty and refresh when + // the user actually switches to it — skips an IPC + full table rebuild per + // batch-done when the user is watching the upload view. + _historyDirty = true; + if (_isHistoryTabActive()) loadHistory(); const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone; if (removeOnDone) { - const doneJobs = queueJobs.filter(j => j.status === 'done'); - for (const job of doneJobs) { - removeJobFromIndex(job); - selectedJobIds.delete(job.id); + // Single pass: build the keep-list and clean up the index for removed jobs. + const nextJobs = []; + for (const job of queueJobs) { + if (job.status === 'done') { + removeJobFromIndex(job); + selectedJobIds.delete(job.id); + } else { + nextJobs.push(job); + } } - queueJobs = queueJobs.filter(j => j.status !== 'done'); + queueJobs = nextJobs; renderQueueTable(); } @@ -3215,24 +3232,50 @@ function updateRecentSortHeaders() { let _recentListenersBound = false; +function _buildRecentRowHtml(row) { + const cls = `recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}`; + return `` + + `${escapeHtml(row.date)}` + + `${escapeHtml(row.filename)}` + + `${escapeHtml(row.host)}` + + `${escapeHtml(row.link)}` + + ``; +} + +// Tracks the last rendered dataset so we can append-only when the user is just +// accumulating new uploads (the default case: sort=date desc, rows only grow). +let _recentLastRenderedSig = ''; +let _recentLastRenderedLen = 0; + function renderRecentUploadsPanel() { const tbody = document.getElementById('recentFilesBody'); if (!tbody) return; if (!sessionFilesData.length) { tbody.innerHTML = 'Noch keine Uploads in dieser Session.'; + _recentLastRenderedSig = ''; + _recentLastRenderedLen = 0; return; } const rows = sortRecentFiles(sessionFilesData); + const sig = `${recentSortState.key}|${recentSortState.direction}`; + const dateDescAppendOnly = sig === 'date|desc' + && _recentLastRenderedSig === sig + && rows.length > _recentLastRenderedLen + && tbody.querySelectorAll('.recent-file-row').length === _recentLastRenderedLen; - tbody.innerHTML = rows.map(row => ` - - ${escapeHtml(row.date)} - ${escapeHtml(row.filename)} - ${escapeHtml(row.host)} - ${escapeHtml(row.link)} - - `).join(''); + if (dateDescAppendOnly) { + // Fast path: only new rows (date desc puts newest on top) — insert them + // at the top without rebuilding the 5000-row tbody below. + const added = rows.length - _recentLastRenderedLen; + let html = ''; + for (let i = 0; i < added; i++) html += _buildRecentRowHtml(rows[i]); + tbody.insertAdjacentHTML('afterbegin', html); + } else { + tbody.innerHTML = rows.map(_buildRecentRowHtml).join(''); + } + _recentLastRenderedSig = sig; + _recentLastRenderedLen = rows.length; // Event delegation – bind once, not per-row if (!_recentListenersBound) {