mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-01-20 20:01:18 +00:00
Merge branch 'main' into lazy-loading-items-index
This commit is contained in:
commit
75e513e5d5
@ -26,7 +26,7 @@
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/react": "^18.2.79",
|
||||
@ -44,12 +44,12 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"globals": "^16.3.0",
|
||||
"globals": "^17.0.0",
|
||||
"postcss": "^8.4.30",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.9.0",
|
||||
"vite": "^7.3.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
409
docs/PERFORMANCE_OPTIMIZATION_PLAN.md
Normal file
409
docs/PERFORMANCE_OPTIMIZATION_PLAN.md
Normal file
@ -0,0 +1,409 @@
|
||||
# Performance-Optimierungsplan für Utopia Map
|
||||
|
||||
## Zusammenfassung der Analyse
|
||||
|
||||
Die Performance-Analyse hat drei Hauptbereiche mit Optimierungspotenzial identifiziert:
|
||||
|
||||
1. **React Component Re-Renders** - Unnötige Re-Renders durch Context-Architektur
|
||||
2. **Map/Leaflet Performance** - Marker-Rendering und Clustering-Konfiguration
|
||||
3. **Animations/Transitions** - CSS-Optimierungen für flüssigere Übergänge
|
||||
|
||||
---
|
||||
|
||||
## Kritische Probleme (Höchste Priorität)
|
||||
|
||||
### 1. MarkerClusterGroup Konfiguration
|
||||
**Datei:** `lib/src/Components/Map/UtopiaMapInner.tsx:329-337`
|
||||
|
||||
```tsx
|
||||
// PROBLEM: Alle Marker bleiben im DOM, auch wenn off-screen
|
||||
removeOutsideVisibleBounds={false}
|
||||
```
|
||||
|
||||
**Fix:** `removeOutsideVisibleBounds={true}` setzen
|
||||
|
||||
### 2. Marker ohne Memoization
|
||||
**Datei:** `lib/src/Components/Item/PopupView.tsx:95-183`
|
||||
|
||||
- Alle Marker werden bei jedem State-Change neu gerendert
|
||||
- Inline Event-Handler erstellen neue Funktionen pro Marker
|
||||
- `MarkerIconFactory` wird für jeden Marker bei jedem Render aufgerufen
|
||||
|
||||
**Fix:**
|
||||
- Marker-Komponente mit `React.memo` wrappen
|
||||
- Event-Handler mit `useCallback` memoizen
|
||||
- Icon-Erstellung mit `useMemo` cachen
|
||||
|
||||
### 3. Item-Mutation während Render
|
||||
**Datei:** `lib/src/Components/Item/PopupView.tsx:98-106`
|
||||
|
||||
```tsx
|
||||
// PROBLEM: Direkte Mutation des Item-Objekts
|
||||
item.text += '\n\n'
|
||||
item.tags.map((tag) => {
|
||||
item.text += `#${encodeTag(tag)}`
|
||||
})
|
||||
```
|
||||
|
||||
**Fix:** Immutable Kopie erstellen oder Berechnung in useMemo verschieben
|
||||
|
||||
---
|
||||
|
||||
## Hohe Priorität
|
||||
|
||||
### 4. Context Provider Nesting
|
||||
**Datei:** `lib/src/Components/AppShell/ContextWrapper.tsx:55-94`
|
||||
|
||||
10 verschachtelte Context-Provider verursachen Kaskaden-Re-Renders.
|
||||
|
||||
**Fix:**
|
||||
- AppState in separate Contexts aufteilen (UI-State, Theme, Assets)
|
||||
- `useMemo` für Context-Values verwenden
|
||||
|
||||
### 5. getItemTags O(n) Suche
|
||||
**Datei:** `lib/src/Components/Map/hooks/useTags.tsx:92-118`
|
||||
|
||||
Lineare Suche für jeden Tag-Match bei jedem Item.
|
||||
|
||||
**Fix:** Map/Set für O(1) Lookups verwenden
|
||||
|
||||
### 6. ProfileForm teure Effect-Berechnung
|
||||
**Datei:** `lib/src/Components/Profile/ProfileForm.tsx:93-143`
|
||||
|
||||
- Mehrere O(n) Array-Suchen
|
||||
- Effect läuft bei jeder Items/Tags Änderung
|
||||
|
||||
**Fix:** `useMemo` für berechnete Werte
|
||||
|
||||
---
|
||||
|
||||
## Mittlere Priorität
|
||||
|
||||
### 7. SetAppState - 4 separate Effects
|
||||
**Datei:** `lib/src/Components/AppShell/SetAppState.tsx:20-34`
|
||||
|
||||
4 separate Effects statt einem gebatchten Update.
|
||||
|
||||
**Fix:** Zu einem Effect zusammenfassen
|
||||
|
||||
### 8. Fehlende React.memo
|
||||
- `HeaderView` - `lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx`
|
||||
- `RelationCard` - `lib/src/Components/Profile/Subcomponents/RelationCard.tsx`
|
||||
- `ItemViewPopup` - `lib/src/Components/Map/Subcomponents/ItemViewPopup.tsx`
|
||||
- `FlexView` Template-Komponenten - `lib/src/Components/Profile/Templates/FlexView.tsx`
|
||||
|
||||
### 9. GalleryView ohne useMemo
|
||||
**Datei:** `lib/src/Components/Profile/Subcomponents/GalleryView.tsx:22-39`
|
||||
|
||||
Images-Array wird bei jedem Render neu berechnet.
|
||||
|
||||
---
|
||||
|
||||
## Animation Optimierungen
|
||||
|
||||
### 10. Zu lange Transition-Zeiten
|
||||
**Datei:** `app/src/App.css:18-21`
|
||||
|
||||
```css
|
||||
/* PROBLEM: 1000ms ist zu langsam */
|
||||
.transition-fade {
|
||||
transition: opacity 1000ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Auf 300-400ms reduzieren
|
||||
|
||||
### 11. Margin statt Transform für Sidebar
|
||||
**Dateien:**
|
||||
- `lib/src/Components/AppShell/SideBar.tsx`
|
||||
- `lib/src/Components/AppShell/Content.tsx`
|
||||
|
||||
```tsx
|
||||
// PROBLEM: Margin-Transitions verursachen Layout-Reflows
|
||||
tw:ml-48 tw:transition-all
|
||||
|
||||
// BESSER: GPU-beschleunigte Transforms
|
||||
tw:translate-x-48 tw:transition-transform
|
||||
```
|
||||
|
||||
### 12. Modal Animation Konflikte
|
||||
**Datei:** `lib/src/Components/Gaming/Modal.tsx:23-24`
|
||||
|
||||
`tw:transition-none` auf modal-box überschreibt Container-Transition.
|
||||
|
||||
### 13. DialogModal DOM-Manipulation
|
||||
**Datei:** `lib/src/Components/Templates/DialogModal.tsx`
|
||||
|
||||
Direkte style/classList Manipulation verursacht Layout-Thrashing.
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Implementierungsreihenfolge
|
||||
|
||||
### Phase 1: Quick Wins (Sofort umsetzbar)
|
||||
1. `removeOutsideVisibleBounds={true}` setzen
|
||||
2. App.css Transition von 1000ms auf 300ms reduzieren
|
||||
3. SetAppState Effects zusammenfassen
|
||||
|
||||
### Phase 2: Memoization (Mittlerer Aufwand)
|
||||
4. Marker-Komponente mit React.memo wrappen
|
||||
5. MarkerIconFactory Ergebnisse cachen
|
||||
6. HeaderView, RelationCard, ItemViewPopup mit React.memo
|
||||
7. GalleryView images mit useMemo
|
||||
|
||||
### Phase 3: Architektur (Höherer Aufwand)
|
||||
8. getItemTags auf Map-basierte Lookups umstellen
|
||||
9. ProfileForm Berechnungen optimieren
|
||||
10. Item-Mutation in PopupView beheben
|
||||
|
||||
### Phase 4: CSS/Animation
|
||||
11. Sidebar Margin → Transform Migration
|
||||
12. Modal Animation Konflikte beheben
|
||||
13. will-change Hints hinzufügen
|
||||
|
||||
---
|
||||
|
||||
## Erwartete Verbesserungen
|
||||
|
||||
- **30-50% weniger Re-Renders** durch Memoization
|
||||
- **Flüssigere Map-Interaktion** durch Marker-Optimierungen
|
||||
- **Schnellere Übergänge** durch kürzere Transition-Zeiten
|
||||
- **Weniger Layout-Thrashing** durch Transform statt Margin
|
||||
|
||||
---
|
||||
|
||||
## Kritische Dateien
|
||||
|
||||
| Datei | Änderungen |
|
||||
|-------|------------|
|
||||
| `UtopiaMapInner.tsx` | Cluster-Config, Event-Handler |
|
||||
| `PopupView.tsx` | Marker-Memoization, Item-Mutation |
|
||||
| `useTags.tsx` | Map-basierte Lookups |
|
||||
| `ProfileForm.tsx` | useMemo für Berechnungen |
|
||||
| `SetAppState.tsx` | Effects zusammenfassen |
|
||||
| `App.css` | Transition-Zeiten |
|
||||
| `SideBar.tsx` / `Content.tsx` | Transform statt Margin |
|
||||
|
||||
---
|
||||
|
||||
## Messstrategie & Erfolgsmetriken
|
||||
|
||||
### Baseline-Messung (vor Optimierungen)
|
||||
|
||||
#### 1. React DevTools Profiler
|
||||
```bash
|
||||
# Chrome Extension: React Developer Tools
|
||||
# Profiler Tab → Record → Interaktionen durchführen → Stop
|
||||
```
|
||||
|
||||
**Zu messende Szenarien:**
|
||||
- [ ] Map laden mit 100+ Markern → Render-Zeit notieren
|
||||
- [ ] Filter-Tag hinzufügen/entfernen → Re-Render-Count
|
||||
- [ ] Sidebar öffnen/schließen → Render-Zeit
|
||||
- [ ] Item-Popup öffnen → Zeit bis vollständig gerendert
|
||||
- [ ] Zwischen Items navigieren → Transition-Smoothness
|
||||
|
||||
**Metriken:**
|
||||
| Szenario | Baseline | Nach Phase 1 | Nach Phase 2 | Ziel |
|
||||
|----------|----------|--------------|--------------|------|
|
||||
| Initial Map Render | ___ ms | ___ ms | ___ ms | <500ms |
|
||||
| Marker Re-Renders | ___ count | ___ count | ___ count | <10 |
|
||||
| Tag Filter Toggle | ___ ms | ___ ms | ___ ms | <100ms |
|
||||
| Sidebar Toggle | ___ ms | ___ ms | ___ ms | <50ms |
|
||||
| Popup Open | ___ ms | ___ ms | ___ ms | <200ms |
|
||||
|
||||
#### 2. Chrome DevTools Performance Panel
|
||||
```
|
||||
F12 → Performance Tab → Record → Interaktionen → Stop
|
||||
```
|
||||
|
||||
**Zu messen:**
|
||||
- **FPS während Animationen** (Ziel: konstant 60 FPS)
|
||||
- **Long Tasks** (>50ms) identifizieren
|
||||
- **Layout Shifts** bei Sidebar/Modal
|
||||
- **Scripting vs Rendering Zeit**
|
||||
|
||||
#### 3. Lighthouse Performance Score
|
||||
```bash
|
||||
# Chrome DevTools → Lighthouse → Performance
|
||||
```
|
||||
|
||||
**Metriken:**
|
||||
| Metrik | Baseline | Ziel |
|
||||
|--------|----------|------|
|
||||
| Performance Score | ___ | >90 |
|
||||
| First Contentful Paint | ___ s | <1.5s |
|
||||
| Largest Contentful Paint | ___ s | <2.5s |
|
||||
| Total Blocking Time | ___ ms | <200ms |
|
||||
| Cumulative Layout Shift | ___ | <0.1 |
|
||||
|
||||
#### 4. Custom Performance Markers (Optional)
|
||||
```tsx
|
||||
// Kann in kritischen Komponenten eingefügt werden:
|
||||
useEffect(() => {
|
||||
performance.mark('PopupView-render-start')
|
||||
return () => {
|
||||
performance.mark('PopupView-render-end')
|
||||
performance.measure('PopupView-render',
|
||||
'PopupView-render-start',
|
||||
'PopupView-render-end')
|
||||
console.log(performance.getEntriesByName('PopupView-render'))
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Testszenarien für Vergleich
|
||||
|
||||
#### Szenario A: Große Datenmenge
|
||||
1. Map mit 200+ Items laden
|
||||
2. 3 Filter-Tags aktivieren
|
||||
3. Zwischen 5 Items navigieren
|
||||
4. Sidebar 3x öffnen/schließen
|
||||
|
||||
#### Szenario B: Schnelle Interaktionen
|
||||
1. Rapid Tag-Toggle (5x schnell hintereinander)
|
||||
2. Item-Popup öffnen → schließen → nächstes öffnen
|
||||
3. Sidebar während Map-Pan öffnen
|
||||
|
||||
#### Szenario C: Animation Smoothness
|
||||
1. Sidebar-Animation beobachten (ruckelt?)
|
||||
2. Modal öffnen/schließen
|
||||
3. Zwischen Profile-Views wechseln
|
||||
|
||||
### Erfolgs-Kriterien
|
||||
|
||||
| Phase | Erfolgskriterium |
|
||||
|-------|------------------|
|
||||
| Phase 1 (Quick Wins) | Lighthouse Score +10 Punkte, keine sichtbaren Ruckler bei Sidebar |
|
||||
| Phase 2 (Memoization) | Re-Render Count -50%, Marker-Interaktion <100ms |
|
||||
| Phase 3 (Architektur) | Initial Load <500ms bei 200 Items |
|
||||
| Phase 4 (CSS) | Konstant 60 FPS bei allen Animationen |
|
||||
|
||||
### Automatisierte Performance-Tests mit Vitest
|
||||
|
||||
#### Setup
|
||||
|
||||
Neue Datei: `lib/src/__tests__/performance.bench.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, bench, expect } from 'vitest'
|
||||
import { render } from '@testing-library/react'
|
||||
import { performance } from 'perf_hooks'
|
||||
|
||||
// Utility für Performance-Messung
|
||||
const measureRender = async (Component: React.FC, props: any) => {
|
||||
const start = performance.now()
|
||||
const { unmount } = render(<Component {...props} />)
|
||||
const end = performance.now()
|
||||
unmount()
|
||||
return end - start
|
||||
}
|
||||
|
||||
describe('Performance Benchmarks', () => {
|
||||
|
||||
// Marker Rendering
|
||||
bench('render 100 markers', async () => {
|
||||
const items = generateMockItems(100)
|
||||
await measureRender(PopupView, { items })
|
||||
}, { time: 1000 })
|
||||
|
||||
bench('render 500 markers', async () => {
|
||||
const items = generateMockItems(500)
|
||||
await measureRender(PopupView, { items })
|
||||
}, { time: 2000 })
|
||||
|
||||
// Tag Filter Performance
|
||||
bench('filter toggle with 100 items', async () => {
|
||||
// Simuliert Tag-Filter Toggle
|
||||
})
|
||||
|
||||
// Sidebar Animation
|
||||
bench('sidebar open/close cycle', async () => {
|
||||
// Misst Sidebar-Toggle
|
||||
})
|
||||
|
||||
// ProfileForm Rendering
|
||||
bench('ProfileForm with complex item', async () => {
|
||||
const item = generateComplexItem()
|
||||
await measureRender(ProfileForm, { item })
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### Vitest Config Erweiterung
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts - benchmark mode hinzufügen
|
||||
export default defineConfig({
|
||||
test: {
|
||||
benchmark: {
|
||||
include: ['**/*.bench.ts'],
|
||||
reporters: ['default', 'json'],
|
||||
outputFile: './benchmark-results.json',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### NPM Script
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test:perf": "vitest bench",
|
||||
"test:perf:baseline": "vitest bench --outputFile=baseline.json",
|
||||
"test:perf:compare": "vitest bench --compare=baseline.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Performance-Grenzwerte
|
||||
|
||||
```typescript
|
||||
// lib/src/__tests__/performance.thresholds.ts
|
||||
export const PERF_THRESHOLDS = {
|
||||
MARKER_RENDER_100: 500, // ms
|
||||
MARKER_RENDER_500: 2000, // ms
|
||||
TAG_FILTER_TOGGLE: 100, // ms
|
||||
SIDEBAR_TOGGLE: 50, // ms
|
||||
POPUP_OPEN: 200, // ms
|
||||
PROFILE_RENDER: 300, // ms
|
||||
}
|
||||
|
||||
// In Tests verwenden:
|
||||
expect(renderTime).toBeLessThan(PERF_THRESHOLDS.MARKER_RENDER_100)
|
||||
```
|
||||
|
||||
#### CI Integration (Optional)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/performance.yml
|
||||
name: Performance Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- run: npm run test:perf
|
||||
- name: Compare with baseline
|
||||
run: npm run test:perf:compare
|
||||
```
|
||||
|
||||
### Dokumentation der Messungen
|
||||
|
||||
Vor jeder Phase:
|
||||
1. Baseline-Werte in Tabelle eintragen
|
||||
2. Screenshots von DevTools Profiler speichern
|
||||
3. Lighthouse Report als PDF exportieren
|
||||
|
||||
Nach jeder Phase:
|
||||
1. Neue Werte eintragen
|
||||
2. Vergleich dokumentieren
|
||||
3. Bei Regression: Ursache analysieren
|
||||
@ -43,7 +43,7 @@
|
||||
"author": "Anton Tranelis",
|
||||
"license": "GPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@rollup/plugin-alias": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
@ -58,8 +58,8 @@
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"cypress": "^15.7.1",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"cypress": "^15.9.0",
|
||||
"daisyui": "^5.5.14",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
@ -73,23 +73,23 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"globals": "^16.3.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"globals": "^17.0.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^3.7.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup": "^4.54.0",
|
||||
"rollup": "^4.55.1",
|
||||
"rollup-plugin-dts": "^6.3.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-svg": "^2.0.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typedoc": "^0.28.15",
|
||||
"typedoc": "^0.28.16",
|
||||
"typedoc-plugin-coverage": "^4.0.2",
|
||||
"typedoc-plugin-missing-exports": "^4.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.9.0",
|
||||
"vite": "^7.3.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
@ -116,7 +116,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.locatecontrol": "^0.79.0",
|
||||
"maplibre-gl": "^5.15.0",
|
||||
"maplibre-gl": "^5.16.0",
|
||||
"radash": "^12.1.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
|
||||
782
package-lock.json
generated
782
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user