From 8b9a40f059c570ca43860edf8f7d2a5b339e36ea Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 22 Apr 2026 22:07:05 +0200 Subject: [PATCH] perf(call): actually unfreeze picker while thumbnails stream in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous pass moved to JPEG + startTransition but the grid still froze. Root causes that survived: 1. React.memo was broken — the parent re-created the inline `onClick={() => onSelect(src.id)}` arrow on every render, so memo's reference check always triggered a fresh render on every card even though nothing visible had changed. Fixed by passing `onSelect` as a stable prop and constructing the click handler inside the memoized child. 2. 20 data-URL `` sources getting decoded more or less at once gave the compositor enough work to make scroll feel laggy. `decoding="async"` punts decode to the browser's image thread; `loading="lazy"` skips it entirely for cards outside the viewport. 3. Concurrency at 4 was still high enough for Windows GDI BitBlt / PrintWindow to contend for the desktop compositor — the whole Tauri window stuttered because the OS-level screen capture was saturating the graphics pipeline. Dropped to 2 concurrent captures; total load takes a touch longer but the picker stays interactive throughout. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/ScreenSourcePicker.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/ScreenSourcePicker.tsx b/apps/desktop/src/components/ScreenSourcePicker.tsx index 0b47381..559e94e 100644 --- a/apps/desktop/src/components/ScreenSourcePicker.tsx +++ b/apps/desktop/src/components/ScreenSourcePicker.tsx @@ -62,7 +62,13 @@ export function ScreenSourcePicker({ open, onClose, onStart }: Props) { return; } let cancelled = false; - const CONCURRENCY = 4; + // Concurrency 2 (down from 4): Windows GDI BitBlt / PrintWindow on + // multiple source windows contends for the desktop compositor and + // the whole Tauri window stutters while 4+ captures are in flight. + // 2 in parallel keeps the compositor breathing and the picker grid + // stays scrollable. Total load time goes up marginally since most + // individual captures are GDI-bound, not thread-bound. + const CONCURRENCY = 2; void (async () => { const list = await listScreenSources(); if (cancelled) return; @@ -310,7 +316,7 @@ function SourceSection({ key={src.id} source={src} selected={selectedId === src.id} - onClick={() => onSelect(src.id)} + onSelect={onSelect} /> ))} @@ -322,20 +328,25 @@ function SourceSection({ // Keeps re-render work proportional to the number of updates instead of // "whole grid on every update" — which was the main reason scrolling felt // frozen during the initial thumbnail fan-in. +// +// The parent passes `onSelect(id)` rather than an inline `onClick`-arrow +// so the callback reference stays stable across renders; otherwise +// React.memo would always see a fresh function prop and re-render every +// card on every parent update. const SourceCard = memo(function SourceCard({ source, selected, - onClick, + onSelect, }: { source: ScreenSource; selected: boolean; - onClick: () => void; + onSelect: (id: string) => void; }) { const thumb = thumbnailDataUrl(source); return (