From c828ab1a48970a396160a0e7e47c0046d5276ab6 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Wed, 25 Feb 2026 14:04:58 +0100 Subject: [PATCH] perf: optimize DB queries, SSE polling, and client rendering - Fix resolveCollaborator N+1: replace full User table scan with findFirst - Fix getAllUsersWithStats N+1: use groupBy instead of per-user count queries - Cache getTeamMemberIdsForAdminTeams and isAdminOfUser with React.cache - Increase SSE poll interval from 1s to 2s across all 5 subscribe routes - Add cleanupOldEvents method to session-share-events for event table TTL - Add React.memo to all card components (Swot, Motivator, Weather, WeeklyCheckIn, YearReview) - Fix WeatherCard useEffect+setState lint error with idiomatic prop sync pattern - Add optimizePackageImports for DnD libs and poweredByHeader:false in next.config - Add inline theme script in layout.tsx to prevent dark mode FOUC - Remove unused Next.js template SVGs from public/ Co-Authored-By: Claude Opus 4.6 --- PERF_OPTIMIZATIONS.md | 74 +++++++++++++++++++ next.config.ts | 9 +++ public/file.svg | 1 - public/globe.svg | 1 - public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - .../api/motivators/[id]/subscribe/route.ts | 2 +- src/app/api/sessions/[id]/subscribe/route.ts | 2 +- src/app/api/weather/[id]/subscribe/route.ts | 2 +- .../weekly-checkin/[id]/subscribe/route.ts | 2 +- .../api/year-review/[id]/subscribe/route.ts | 2 +- src/app/layout.tsx | 7 ++ .../moving-motivators/MotivatorCard.tsx | 9 ++- src/components/swot/SwotCard.tsx | 7 +- src/components/weather/WeatherCard.tsx | 21 +++--- .../weekly-checkin/WeeklyCheckInCard.tsx | 7 +- src/components/year-review/YearReviewCard.tsx | 7 +- src/services/auth.ts | 56 +++++++------- src/services/session-share-events.ts | 11 +++ src/services/teams.ts | 10 ++- 21 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 PERF_OPTIMIZATIONS.md delete mode 100644 public/file.svg delete mode 100644 public/globe.svg delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg diff --git a/PERF_OPTIMIZATIONS.md b/PERF_OPTIMIZATIONS.md new file mode 100644 index 0000000..e6ac2b6 --- /dev/null +++ b/PERF_OPTIMIZATIONS.md @@ -0,0 +1,74 @@ +# Optimisations de performance + +## Requêtes DB (impact critique) + +### resolveCollaborator — suppression du scan complet de la table User +**Fichier:** `src/services/auth.ts` + +Avant : `findMany` sur tous les users puis `find()` en JS pour un match case-insensitive par nom. +Après : `findFirst` avec `contains` + vérification exacte. O(1) au lieu de O(N users). + +### getAllUsersWithStats — suppression du N+1 +**Fichier:** `src/services/auth.ts` + +Avant : 2 queries `count` par utilisateur (`Promise.all` avec map). +Après : 2 `groupBy` en bulk + construction d'une Map. 3 queries au lieu de 2N+1. + +### React.cache sur les fonctions teams +**Fichier:** `src/services/teams.ts` + +`getTeamMemberIdsForAdminTeams` et `isAdminOfUser` wrappées avec `React.cache()`. +Sur la page `/sessions`, ces fonctions étaient appelées ~10 fois par requête (5 workshop types × 2). Maintenant dédupliquées en 1 appel. + +## SSE / Temps réel (impact haut) + +### Polling interval 1s → 2s +**Fichiers:** 5 routes `src/app/api/*/[id]/subscribe/route.ts` + +Réduit de 50% le nombre de queries DB en temps réel. Imperceptible côté UX (la plupart des outils collab utilisent 2-5s). + +### Nettoyage des events +**Fichier:** `src/services/session-share-events.ts` + +Ajout de `cleanupOldEvents(maxAgeHours)` pour purger les events périmés. Les tables d'events n'ont pas de mécanisme de TTL — cette méthode peut être appelée périodiquement ou à la connexion SSE. + +## Rendu client (impact haut) + +### React.memo sur les composants de cartes +**Fichiers:** +- `src/components/swot/SwotCard.tsx` +- `src/components/moving-motivators/MotivatorCard.tsx` (+ `MotivatorCardStatic`) +- `src/components/weather/WeatherCard.tsx` +- `src/components/weekly-checkin/WeeklyCheckInCard.tsx` +- `src/components/year-review/YearReviewCard.tsx` + +Ces composants sont rendus en liste et re-rendaient tous à chaque drag, changement d'état, ou `router.refresh()` SSE. + +### WeatherCard — fix du pattern useEffect + setState +**Fichier:** `src/components/weather/WeatherCard.tsx` + +Remplacé le `useEffect` qui appelait 5 `setState` (cascading renders, erreur lint React 19) par le pattern idiomatique de state-driven prop sync (comparaison directe dans le render body). + +## Configuration Next.js (impact moyen) + +### next.config.ts +**Fichier:** `next.config.ts` + +- `poweredByHeader: false` — supprime le header `X-Powered-By` (sécurité) +- `optimizePackageImports` — tree-shaking amélioré pour `@dnd-kit/*` et `@hello-pangea/dnd` + +### Fix FOUC dark mode +**Fichier:** `src/app/layout.tsx` + +Script inline dans `` qui lit `localStorage` et applique la classe `dark`/`light` sur `` avant l'hydratation React. Élimine le flash blanc pour les utilisateurs en dark mode. + +## Nettoyage + +- Suppression de 5 SVGs inutilisés du template Next.js (`file.svg`, `globe.svg`, `next.svg`, `vercel.svg`, `window.svg`) + +## Non traité (pour plus tard) + +- **Migration DnD** : consolider `@hello-pangea/dnd` et `@dnd-kit` en une seule lib (~45KB économisés) — 3 boards à réécrire +- **Split WorkshopTabs** (879 lignes) — découper en sous-composants par type +- **Suspense boundaries** sur les pages de détail de session +- **Appel périodique de `cleanupOldEvents`** — à brancher via cron ou à la connexion SSE diff --git a/next.config.ts b/next.config.ts index 68a6c64..23994ac 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + poweredByHeader: false, + experimental: { + optimizePackageImports: [ + "@dnd-kit/core", + "@dnd-kit/sortable", + "@dnd-kit/utilities", + "@hello-pangea/dnd", + ], + }, }; export default nextConfig; diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/api/motivators/[id]/subscribe/route.ts b/src/app/api/motivators/[id]/subscribe/route.ts index 31bddab..52e1341 100644 --- a/src/app/api/motivators/[id]/subscribe/route.ts +++ b/src/app/api/motivators/[id]/subscribe/route.ts @@ -77,7 +77,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: // Connection might be closed clearInterval(pollInterval); } - }, 1000); // Poll every second + }, 2000); // Poll every 2 seconds // Cleanup on abort request.signal.addEventListener('abort', () => { diff --git a/src/app/api/sessions/[id]/subscribe/route.ts b/src/app/api/sessions/[id]/subscribe/route.ts index 93f08bd..095971c 100644 --- a/src/app/api/sessions/[id]/subscribe/route.ts +++ b/src/app/api/sessions/[id]/subscribe/route.ts @@ -77,7 +77,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: // Connection might be closed clearInterval(pollInterval); } - }, 1000); // Poll every second + }, 2000); // Poll every 2 seconds // Cleanup on abort request.signal.addEventListener('abort', () => { diff --git a/src/app/api/weather/[id]/subscribe/route.ts b/src/app/api/weather/[id]/subscribe/route.ts index 99a894a..9150927 100644 --- a/src/app/api/weather/[id]/subscribe/route.ts +++ b/src/app/api/weather/[id]/subscribe/route.ts @@ -80,7 +80,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: // Connection might be closed clearInterval(pollInterval); } - }, 1000); // Poll every second + }, 2000); // Poll every 2 seconds // Cleanup on abort request.signal.addEventListener('abort', () => { diff --git a/src/app/api/weekly-checkin/[id]/subscribe/route.ts b/src/app/api/weekly-checkin/[id]/subscribe/route.ts index ee44290..86fd3b9 100644 --- a/src/app/api/weekly-checkin/[id]/subscribe/route.ts +++ b/src/app/api/weekly-checkin/[id]/subscribe/route.ts @@ -80,7 +80,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: // Connection might be closed clearInterval(pollInterval); } - }, 1000); // Poll every second + }, 2000); // Poll every 2 seconds // Cleanup on abort request.signal.addEventListener('abort', () => { diff --git a/src/app/api/year-review/[id]/subscribe/route.ts b/src/app/api/year-review/[id]/subscribe/route.ts index 30cd4ba..a8b19d7 100644 --- a/src/app/api/year-review/[id]/subscribe/route.ts +++ b/src/app/api/year-review/[id]/subscribe/route.ts @@ -80,7 +80,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: // Connection might be closed clearInterval(pollInterval); } - }, 1000); // Poll every second + }, 2000); // Poll every 2 seconds // Cleanup on abort request.signal.addEventListener('abort', () => { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f95b85b..1316ba1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -29,6 +29,13 @@ export default function RootLayout({ }>) { return ( + +