From d50e22b62d9b23398c4c73f76278557248d9179f Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 2 Jul 2026 23:02:11 +0200 Subject: [PATCH] perf: stop per-cover HttpClient churn (Android) + scroll-arrow reflow (web) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android: MusicRepository.coverArtUrl/streamUrl built a fresh SubsonicClient per call — each spins up (and never closes) a Ktor HttpClient. That ran for every cover on every recomposition, causing heavy GC/alloc churn (and a leak) that showed up as scroll/tab lag. Reuse the cached ClientHolder client for URL building instead. Web: the home scroll-row arrows called getBoundingClientRect on every scroll event (layout thrashing). Split it: only toggle arrow visibility on scroll (rAF-throttled); do the getBoundingClientRect vertical-centering only on resize/content change. Co-Authored-By: Claude Opus 4.8 --- .../com/novasonic/app/data/MusicRepository.kt | 7 ++++--- src/ui/components.js | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt index 0ef7932..07e5bfb 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt @@ -76,8 +76,9 @@ class MusicRepository(private val context: Context) { } } val creds = lastCredentials ?: return null - return SubsonicClient(creds.serverUrl, creds.username, creds.password) - .coverArtUrl(id ?: return null, size) + // Reuse the cached client — building a fresh SubsonicClient here spins up a whole Ktor + // HttpClient per cover (and never closes it), which is heavy GC/alloc churn while scrolling. + return ClientHolder.get(creds).coverArtUrl(id ?: return null, size) } fun streamUrl(id: String): String? { @@ -86,7 +87,7 @@ class MusicRepository(private val context: Context) { if (f.exists()) return Uri.fromFile(f).toString() } val creds = lastCredentials ?: return null - return SubsonicClient(creds.serverUrl, creds.username, creds.password).streamUrl(id) + return ClientHolder.get(creds).streamUrl(id) // reuse cached client (no per-call HttpClient) } private fun offlineRepo(): OfflineRepository? = runCatching { diff --git a/src/ui/components.js b/src/ui/components.js index 96ce7d9..76e3fa2 100644 --- a/src/ui/components.js +++ b/src/ui/components.js @@ -131,24 +131,31 @@ export function enhanceScrollRow(row) { const right = mkBtn('sa-right', _CHEV_R, 'Weiter'); const EPS = 4; - const update = () => { + // Cheap per-scroll work — just toggle which arrow is available; rAF-throttled to once per frame. + let _rafPending = false; + const updateArrows = () => { + _rafPending = false; left.classList.toggle('can-scroll', row.scrollLeft > EPS); right.classList.toggle('can-scroll', row.scrollLeft < row.scrollWidth - row.clientWidth - EPS); - // Vertically center the arrows on the cover art (fallback: CSS default 72px) + }; + const onScroll = () => { if (!_rafPending) { _rafPending = true; requestAnimationFrame(updateArrows); } }; + // Expensive (getBoundingClientRect forces layout) — only on size/content change, NOT every scroll. + const positionArrows = () => { const art = row.querySelector('.card-art') || row.querySelector('.card'); if (art) { const top = art.getBoundingClientRect().top - wrap.getBoundingClientRect().top + art.offsetHeight / 2; if (top > 0) { left.style.top = right.style.top = top + 'px'; } } + updateArrows(); }; const step = () => Math.max(160, row.clientWidth * 0.8); left.addEventListener('click', () => row.scrollBy({ left: -step(), behavior: 'smooth' })); right.addEventListener('click', () => row.scrollBy({ left: step(), behavior: 'smooth' })); - row.addEventListener('scroll', update, { passive: true }); - new ResizeObserver(update).observe(row); - new MutationObserver(update).observe(row, { childList: true }); - update(); + row.addEventListener('scroll', onScroll, { passive: true }); + new ResizeObserver(positionArrows).observe(row); + new MutationObserver(positionArrows).observe(row, { childList: true }); + positionArrows(); } /** Enhance every `.scroll-row` currently in the DOM (the static Home rows). */