From 17115dc8e7f9a856bdf2cae5f2db5d7f8f2f6f96 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Thu, 15 Oct 2026 22:35:16 +0800 Subject: [PATCH] Search: clear the previous result line while a new search loads, draw long lists in chunks, append the full list instead of redrawing, warm fewer thumbnails --- frontend/app.js | 18 +++++++++++++++--- frontend/views-core.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 5596c38..1c73537 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -1894,7 +1894,9 @@ function startCacheResyncWatch() { // Pull a result set's artwork into the service worker's thumbnail cache, a few at // a time, so a saved search still has pictures offline. function warmSearchThumbs(results) { - const urls = (results || []).map((r) => r && r.thumbnail).filter(Boolean).slice(0, 100); + // Only the first screens: warming 100+ images competes with the results + // themselves on a slow link. + const urls = (results || []).map((r) => r && r.thumbnail).filter(Boolean).slice(0, 24); let i = 0; const next = () => { if (i >= urls.length) return; warmThumb(urls[i++]); setTimeout(next, 40); }; next(); next(); next(); @@ -8067,6 +8069,9 @@ function wireUI() { collectSearchMetadata(q, instant.results); if (SearchLibrary.isFresh(instant.ts)) { scrollListIntoViewPortrait(); return; } } else { + // Never leave the previous search's "Updated … · N results" line over + // an empty list while this one loads. + $('searchMeta')?.classList.add('hidden'); render(); showSearchSkeletons(); } @@ -8120,8 +8125,15 @@ function wireUI() { const more = await API.search(q); const full = merged(more?.results || []); if (mySeq !== searchSeq || !more?.ok || full.length <= results.length) return; - searchResults = full; - renderList(); + const shown = searchResults; + const prefix = shown.length <= full.length && shown.every((v, i) => full[i] && full[i].id === v.id); + if (prefix && els.cards.children.length) { + // Grow the same array, so cards already drawn queue the full list too. + const from = shown.length; + shown.push(...full.slice(from)); + searchResults = shown; + appendListCards(shown, from); + } else { searchResults = full; renderList(); } RecentSearches.cacheResults(q, full, Date.now()); showSearchMeta(q, Date.now()); } catch { /* keep the first page */ } diff --git a/frontend/views-core.js b/frontend/views-core.js index 3582a2d..2eff79d 100644 --- a/frontend/views-core.js +++ b/frontend/views-core.js @@ -405,8 +405,38 @@ function renderList() { } els.status.classList.add('hidden'); - list.forEach((v, i) => els.cards.appendChild(renderCard(v, i, list))); + // Long lists (a deep search returns up to 200) are drawn in chunks: the first + // screenful at once, the rest on later frames, so a phone never freezes. + const token = (renderList._token = (renderList._token || 0) + 1); + const FIRST = 30, STEP = 40; + const draw = (from, to) => { + const frag = document.createDocumentFragment(); + for (let i = from; i < Math.min(to, list.length); i++) frag.appendChild(renderCard(list[i], i, list)); + els.cards.appendChild(frag); + }; + draw(0, FIRST); markPlayingCard(); + let at = FIRST; + const more = () => { + if (renderList._token !== token || at >= list.length) return; + draw(at, at + STEP); at += STEP; markPlayingCard(); + (window.requestIdleCallback || ((f) => setTimeout(f, 30)))(more); + }; + if (list.length > FIRST) (window.requestIdleCallback || ((f) => setTimeout(f, 30)))(more); +} +// Search only: the server's fuller list arrived and starts with the cards +// already shown — add the new ones below instead of redrawing everything. +function appendListCards(list, fromIndex) { + const token = renderList._token; + let at = fromIndex; + const step = () => { + if (renderList._token !== token || at >= list.length) return; + const frag = document.createDocumentFragment(); + for (const end = Math.min(at + 40, list.length); at < end; at++) frag.appendChild(renderCard(list[at], at, list)); + els.cards.appendChild(frag); + (window.requestIdleCallback || ((f) => setTimeout(f, 30)))(step); + }; + step(); } function addBrowseCta(empty, label) {