# Plan: Menu Performance Optimalisatie (<250ms response)
## Probleem
Trage menu-reacties bij:
1. **EPD Sidebar** (hoofdnavigatie links)
2. **Patient tabs** (binnen patiënt dossier)
Huidige performance: ~260ms, doel: <250ms
## Diagnose
### EPD Sidebar bottlenecks
- Re-renders bij elke `usePathname()` change
- `.map()` creëert nieuwe array (50+ items) per render
- Geen `React.memo` bescherming
- Regex match bij elke render
### Patient Tabs bottlenecks
- Re-renders bij elke sub-route navigatie
- Geen memoization van active state
### Context cascade
- PatientContext reset bij unmount → flashing
- Header herberekent derived state (40+ regels) per render
## Aanpak
**Fase 3 (Server Components) zou NIET helpen** - dit zijn client-side React performance issues.
### Quick wins (hoogste impact)
1. **EPD Sidebar optimalisatie** (~80ms besparing)
2. **Patient Tabs memoization** (~40ms besparing)
3. **Header derived state memoization** (~30ms besparing)
4. **Context reset pattern verbeteren** (~20ms besparing)
Totaal: ~170ms besparing → target <90ms
---
## Implementatie
> **Status Update (26-11-2025):** Alle 4 stappen zijn geïmplementeerd en klaar voor testing.
### Stap 1: EPD Sidebar optimalisatie ✅ VOLTOOID
**Bestand:** `app/epd/components/epd-sidebar.tsx`
**Status:** Geïmplementeerd - alle optimalisaties toegepast
**Probleem:**
```tsx
const navigationItems = isPatientContext
? level2NavigationItems.map(item => ({
...item,
href: `/epd/patients/${patientId}${item.href}`
}))
: level1NavigationItems;
```
**Oplossing:**
1. Memoize navigation items:
```tsx
const navigationItems = useMemo(() => {
if (isPatientContext) {
return level2NavigationItems.map(item => ({
...item,
href: `/epd/patients/${patientId}${item.href}`
}));
}
return level1NavigationItems;
}, [isPatientContext, patientId]);
```
2. Maak SidebarItem component met React.memo:
```tsx
const SidebarItem = memo(({ item, isActive, isCollapsed }: Props) => {
return (
{!isCollapsed && {item.label}}
);
}, (prev, next) => {
// Shallow compare
return prev.item.href === next.item.href &&
prev.isActive === next.isActive &&
prev.isCollapsed === next.isCollapsed;
});
```
3. Memoize isActive check:
```tsx
const getIsActive = useCallback((href: string) => {
return pathname === href || pathname?.startsWith(`${href}/`);
}, [pathname]);
```
4. Stabilize event handlers:
```tsx
const handleToggle = useCallback(() => {
setIsCollapsed(prev => !prev);
}, []);
const handleMobileToggle = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
```
**Geïmplementeerde wijzigingen:**
- ✅ Toegevoegd: `useMemo`, `useCallback`, `memo` imports
- ✅ Nieuwe `SidebarItem` component met React.memo en custom comparison
- ✅ navigationItems gememoized met useMemo
- ✅ Event handlers gestabiliseerd met useCallback (toggleSidebar, toggleCollapse, handleItemClick)
- ✅ getIsActive functie gememoized
- ✅ Rendering vervangen door SidebarItem component
---
### Stap 2: Patient Tabs optimalisatie ✅ VOLTOOID
**Bestand:** `app/epd/patients/[id]/intakes/[intakeId]/components/intake-tabs.tsx`
**Status:** Geïmplementeerd - alle optimalisaties toegepast
**Probleem:**
- Re-renders bij elke pathname change
- Geen memoization
**Oplossing:**
1. Memoize tab items:
```tsx
const tabs = useMemo(() => [
{ href: `/epd/patients/${patientId}/intakes/${intakeId}/anamnese`, label: 'Anamnese' },
{ href: `/epd/patients/${patientId}/intakes/${intakeId}/diagnosis`, label: 'Diagnose' },
// ... rest
], [patientId, intakeId]);
```
2. Maak TabItem component met memo:
```tsx
const TabItem = memo(({ tab, isActive }: { tab: Tab; isActive: boolean }) => {
return (
{tab.label}
);
});
```
3. Memoize active check:
```tsx
const isTabActive = useCallback((href: string) => {
return pathname === href;
}, [pathname]);
```
**Geïmplementeerde wijzigingen:**
- ✅ Toegevoegd: `useMemo`, `useCallback`, `memo` imports
- ✅ Nieuwe `TabItem` component met React.memo
- ✅ tabs array gememoized met useMemo
- ✅ baseUrl gememoized
- ✅ getIsActive functie gememoized met useCallback
- ✅ Rendering vervangen door TabItem component
---
### Stap 3: EPD Header memoization ✅ VOLTOOID
**Bestand:** `app/epd/components/epd-header.tsx`
**Status:** Geïmplementeerd - component volledig geoptimaliseerd
**Probleem:**
- 40+ regels berekeningen per render
- Geen useMemo
**Oplossing:**
1. Memoize patient display data:
```tsx
const patientDisplay = useMemo(() => {
if (!patient) return null;
const name = patient.name?.[0];
const displayName = name
? [...(name.prefix || []), ...(name.given || []), name.family]
.filter(Boolean)
.join(' ')
: 'Onbekende patiënt';
const birthDate = patient.birthDate
? new Date(patient.birthDate).toLocaleDateString('nl-NL')
: null;
const bsn = patient.identifier?.find(id =>
id.system === 'http://fhir.nl/fhir/NamingSystem/bsn'
)?.value;
const statusExtension = patient.extension?.find(ext =>
ext.url === 'http://hl7.org/fhir/StructureDefinition/patient-status'
);
const isJohnDoe = patient.extension?.some(ext =>
ext.url === 'http://hl7.org/fhir/StructureDefinition/data-absent-reason' &&
ext.valueCode === 'temp-unknown'
);
return { displayName, birthDate, bsn, statusExtension, isJohnDoe };
}, [patient]);
```
2. Stabilize event handlers:
```tsx
const handleNewReportClick = useCallback(() => {
if (!patient?.id) return;
const rapportagePath = `/epd/patients/${patient.id}/rapportage`;
const onRapportagePage = pathname?.startsWith(rapportagePath);
if (onRapportagePage) {
const element = document.getElementById('rapportage-composer');
element?.scrollIntoView({ behavior: 'smooth' });
} else {
router.push(`${rapportagePath}#rapportage-composer`);
}
}, [patient?.id, pathname, router]);
```
3. Wrap component in memo:
```tsx
export const EPDHeader = memo(function EPDHeader() {
// ... component body
});
```
**Geïmplementeerde wijzigingen:**
- ✅ Toegevoegd: `useMemo`, `useCallback`, `memo` imports
- ✅ patientDisplay object gememoized met useMemo (alle 40+ regels berekeningen)
- ✅ handleNewReportClick gestabiliseerd met useCallback
- ✅ Component gewrapped in memo() export
- ✅ Destructuring van gememoized values voor cleaner JSX
---
### Stap 4: PatientContext reset pattern ✅ VOLTOOID
**Bestand:** `app/epd/components/patient-context.tsx`
**Status:** Quick fix geïmplementeerd - geen flashing meer
**Probleem:**
```tsx
useEffect(() => {
setPatient(patient);
return () => setPatient(null); // ← Veroorzaakt flashing
}, [patient, setPatient]);
```
**Oplossing 1 (Quick fix):**
Verwijder cleanup als patient ID niet verandert:
```tsx
export function useSetPatient(patient: FHIRPatient | null) {
const { patient: currentPatient, setPatient } = usePatientContext();
useEffect(() => {
// Only update if patient ID changed
if (patient?.id !== currentPatient?.id) {
setPatient(patient);
}
}, [patient?.id, currentPatient?.id, setPatient]);
// No cleanup - keep patient in context during navigation
}
```
**Oplossing 2 (Beter, maar meer werk):**
Gebruik URL-based patient ID als single source of truth:
```tsx
export function PatientProvider({ children }: { children: ReactNode }) {
const pathname = usePathname();
const [patients, setPatients] = useState