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

98 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: PROJ-78
title: SearchResultsTable re-rendert bei jedem Tastenanschlag im Suchfeld
status: In Review
created: 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.memo` — `src/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
- [x] 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.)
- [x] 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.