Files
archivmail/features/PROJ-78-search-results-rerender-perf.md
T
sysopsandClaude Sonnet 5 16bf38876f perf(PROJ-78): Suchfeld-Debounce + stabile Callbacks gegen Re-Render-Spam
Jeder Tastendruck im Suchfeld setzte bisher sofort State in SearchPage,
was über eine instabile doSearch-Referenz einen vollen Re-Render der
Ergebnistabelle auslöste (bei page_size 100: 100 Zeilen inkl.
sanitizeSnippet pro Tastendruck). Suchbegriff liegt jetzt lokal in der
Filterbar und wird debounced (300ms) nach oben propagiert; doSearch liest
Filterwerte aus einem Ref statt sie als Dependencies zu führen und bleibt
dadurch referenzstabil; SearchResultsTable ist jetzt memoisiert.

Enter-Verhalten bleibt sofort (Debounce wird vor dem Submit geflusht),
übrige Filter unverändert direkt-kontrolliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
2026-08-05 15:11:28 +02:00

4.5 KiB
Raw Blame History

id, title, status, created
id title status created
PROJ-78 SearchResultsTable re-rendert bei jedem Tastenanschlag im Suchfeld In Review 2026-08-05

Problem

SearchResultsTable re-rendert bei jedem Tastenanschlag im Suchfeld, da query in SearchPage liegt und die Tabelle nicht memoisiert ist. Bei page_size 100 sind das 100 Zeilen inkl. sanitizeSnippet-Aufruf pro Tastendruck.

React.memo allein würde hier nichts bringen: onPageChange={(p) => doSearch(p)} ist inline definiert, und doSearch selbst ist ein useCallback mit allen Filter-States in den Dependencies — die Memo-Prüfung würde bei jedem Tastendruck ohnehin fehlschlagen, weil sich die Callback-Referenz ändert.

Ein sauberer Fix bräuchte einen Ref-basierten Umbau von useSearch (z.B. doSearch per useRef stabil halten oder Debounce am Input statt an jedem State-Update). Das ist eine Architekturänderung des Such-Hooks und war im Rahmen der reinen Crash-Härtung-Session ausgeschlossen.

Nur relevant bei großen Ergebnislisten/schwacher Client-Hardware — kein Crash-Risiko.

Lösung (Vorschlag)

  1. useSearch: doSearch stabil halten, z.B. per useRef für die Filter-State-Werte statt sie als useCallback-Dependencies zu führen, oder
  2. Debounce direkt am Such-Input (z.B. 300ms) statt bei jedem Tastendruck sofort neu zu rendern/suchen — reduziert sowohl Re-Renders als auch API-Calls.
  3. SearchResultsTable mit React.memo + stabilen Callback-Referenzen umschließen, sobald 1 oder 2 umgesetzt ist.

Implementation Notes

Umgesetzt wurde eine Kombination aus Vorschlag 13, bewusst minimal gehalten (kein Context, kein Reducer-Umbau):

1. Debounce am Suchfeld — src/components/search/SearchFilterBar.tsx:96-124 Der Suchbegriff wird jetzt lokal in der Filterbar gehalten (localQuery) und nur debounced (300 ms) per setQuery nach oben propagiert. Ein syncedRef merkt sich den zuletzt zwischen Parent und Input abgeglichenen Wert, damit externe Änderungen (gespeicherte Suche anwenden) weiterhin ins Feld durchschlagen, ohne eine Rückkopplungsschleife zu erzeugen. Wirkung: State-Update in SearchPage passiert max. alle 300 ms statt pro Tastendruck — die gesamte Seite inkl. Tabelle rendert entsprechend seltener.

2. doSearch referenzstabil — src/hooks/useSearch.ts:25-71 Die Filterwerte werden in filtersRef gespiegelt; doSearch hat jetzt leere Dependencies und liest ausschließlich filtersRef.current. Damit ändert sich die Callback-Referenz nie mehr. setQuery ist ein Wrapper (useSearch.ts:47-50), der den Wert synchron in den Ref schreibt und erst dann den State setzt — dadurch verwendet ein unmittelbar folgendes doSearch(1) bereits den neuen Suchbegriff.

3. React.memosrc/components/search/SearchResultsTable.tsx:65,190 Komponente in SearchResultsTableInner umbenannt und als memo(SearchResultsTableInner) exportiert. In src/app/search/page.tsx:245 wurde das inline onPageChange={(p) => doSearch(p)} durch onPageChange={doSearch} ersetzt. Alle Props der Tabelle (results, selected, setSelected, total, page, totalPages, onPageChange) sind beim Tippen jetzt referenzstabil → der Memo-Vergleich greift, die 100 Zeilen inkl. sanitizeSnippet werden nicht neu gerendert.

Enter-Verhalten bewusst erhalten: handleSubmit in der Filterbar flusht den Debounce (setQuery(localQuery)) vor dem Aufruf von onSubmit — Enter direkt nach dem Tippen sucht weiterhin sofort mit dem gerade eingegebenen Begriff.

Nicht geändert: Die übrigen Filter (Von/An/Datum/Sortierung/Anhang) bleiben unverändert direkt-kontrolliert (kein Debounce), Pagination, gespeicherte Suchen, Export- und Upload-Dialoge, useSavedSearches und der Initial-Load-Effekt in useSearch.

Verifikation: npm run build (inkl. TypeScript) erfolgreich.

Acceptance Criteria

  • Tippen im Suchfeld löst kein Re-Render der Ergebnistabelle mehr aus, solange keine neue Suche ausgelöst wurde. (Doppelt abgesichert: Debounce verhindert das State-Update in SearchPage, React.memo + stabile Props fangen den verbleibenden Fall ab.)
  • Bestehendes Verhalten (Suche bei Enter/Debounce-Ende, Pagination) funktional unverändert — Enter flusht den Debounce vor der Suche.
  • Messbare Verbesserung (z.B. React DevTools Profiler) bei page_size 100. Nicht automatisiert durchführbar — React DevTools Profiler ist im Agent-Kontext nicht verfügbar. Begründung per Code-Analyse siehe Implementation Notes; manuelle Messung offen für QA.