From 13b4b6971cc1a3d16fc3123a0af316f56e07b271 Mon Sep 17 00:00:00 2001 From: Ladebeze66 Date: Tue, 28 Apr 2026 10:33:07 +0200 Subject: [PATCH] begin_opti --- docs-site-interne/09-performances-images.md | 346 ++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 docs-site-interne/09-performances-images.md diff --git a/docs-site-interne/09-performances-images.md b/docs-site-interne/09-performances-images.md new file mode 100644 index 0000000..cd7e628 --- /dev/null +++ b/docs-site-interne/09-performances-images.md @@ -0,0 +1,346 @@ +# Audit performances images & dev mode + +**Dernière mise à jour :** 2026-04-28 +**Statut :** diagnostic — aucune modification du code n'est encore appliquée. + +> Document d'analyse à l'origine d'un futur lot de corrections. À mettre à jour +> au fur et à mesure que les actions sont réalisées (cocher les cases). + +## 1. Contexte du problème + +Sur certains navigateurs (notamment ceux qui ne sont pas Chromium ou en +connexion limitée), les pages `portfolio/`, `competences/` et la home mettent +plusieurs secondes à afficher leurs visuels. L'hypothèse de départ était : + +- **« les images devraient déjà être en WebP »** ; +- **« Strapi en dev ralentit la livraison des images »** ; +- **« Next + Strapi tournent en `dev`, ce qui dégrade les perfs »**. + +Le diagnostic ci-dessous valide partiellement ces hypothèses et en révèle +d'autres, plus structurelles, qui pèsent davantage que le mode `dev`. + +## 2. Inventaire des médias Strapi (mesure) + +Mesures faites sur `cmsbackend/public/uploads/` (28/04/2026). + +### 2.1 Vue globale + +| Catégorie | Fichiers | Poids | +|---|---:|---:| +| **Total uploads** | 2 603 | **1 034,6 MB** | +| Originaux (sans variantes responsive) | 523 | 569,6 MB | +| Variantes responsive Strapi (`thumbnail_/small_/medium_/large_`) | 2 079 | 465,0 MB | + +### 2.2 Originaux par format + +| Extension | Fichiers | Poids | Statut | +|---|---:|---:|---| +| `.webp` | 252 | 92,5 MB | ✅ converti | +| `.jpg` | 59 | 39,4 MB | ⚠️ à convertir | +| `.png` | 212 | **437,6 MB** | 🔴 à convertir en priorité | + +**190 PNG dépassent 1 MB** (424 MB cumulés), avec un top 10 entre 3 et 4 MB +par fichier (ex. `vase_*.png` à 4,25 MB, illustrations Midjourney à 3,5 MB). + +> **Constat n° 1 :** l'hypothèse « tout est déjà en WebP » est fausse. **271 +> originaux non-WebP** représentent **84 %** du poids des originaux. La +> conversion ciblée des PNG > 1 MB suffirait à diviser le total par 3 ou 4. + +### 2.3 Variantes responsive Strapi + +| Variante | Fichiers | Poids | +|---|---:|---:| +| `large_*` | 518 | 242,9 MB | +| `medium_*` | 518 | 139,6 MB | +| `small_*` | 520 | 65,7 MB | +| `thumbnail_*` | 523 | 16,9 MB | + +Strapi génère bien ces variantes — mais **dans le format de l'original**. Donc +un `.png` de 4 MB produit un `large_…png` de ~1 MB, alors qu'une version +WebP serait à ~150-300 KB pour la même qualité perçue. + +## 3. Comment les images sont consommées par Next + +### 3.1 Toujours `img.url` (l'original), jamais les variantes + +`app/portfolio/page.jsx` (vignettes en grille) : + +```132:137:app/portfolio/page.jsx + {firstImage.alt} +``` + +Même pattern dans : + +- `app/competences/page.jsx` (ligne ~127) +- `app/competences/[slug]/page.tsx` (ligne ~229) +- `app/components/Carousel.tsx` (ligne ~81 + lightbox 120) +- `app/components/CarouselCompetences.tsx` (ligne ~69 + lightbox 108) +- `app/components/VignetteCarousel.tsx` (ligne ~58) +- `app/page.tsx` (portrait hero, ligne 124) + +**Aucun de ces composants ne lit `formats.thumbnail.url`, `formats.small.url`, +`formats.medium.url` ni `formats.large.url`** — Strapi a déjà fait le travail +de redimensionnement, on l'ignore. + +> **Constat n° 2 :** une carte de portfolio en grille charge un PNG ~3 MB +> alors qu'on l'affiche en 400×300 px. La variante `medium_` (~300 KB) ou +> `small_` (~120 KB) suffirait — gain immédiat de **~10× sur le poids +> transféré**, sans toucher aux fichiers stockés. + +### 3.2 `` natif partout — `next/image` jamais utilisé + +`grep` confirme : **aucun `import Image from "next/image"`** dans tout `app/`. +Conséquence : + +- pas de `srcset`/`sizes` automatique (le navigateur télécharge la même image + en mobile qu'en desktop) ; +- pas de placeholder `blur` ; +- pas de conversion à la volée vers AVIF/WebP via le runtime image de Next ; +- aucune dimension donnée (`width`/`height`) → CLS (Cumulative Layout Shift) + potentiel pendant le chargement. + +`next.config.ts` autorise pourtant `localhost` et `api.fernandgrascalvet.com` +dans `images.domains` — la migration est donc partiellement amorcée mais +inutilisée : + +```24:26:next.config.ts + images: { + domains: ["localhost", "api.fernandgrascalvet.com"], + }, +``` + +Note : `domains` est **déprécié** depuis Next 14 ; il faudra basculer vers +`remotePatterns` au moment de la migration (et préciser +`formats: ['image/avif', 'image/webp']`). + +### 3.3 Toutes les pages sont `"use client"` + `useEffect`-fetch + +`app/page.tsx`, `app/portfolio/page.jsx`, `app/competences/page.jsx`, +`app/competences/[slug]/page.tsx` : toutes commencent par `"use client"` et +font `fetch()` côté navigateur dans `useEffect`. Conséquences : + +1. **First paint sans données** → on voit toujours le spinner ou les + squelettes, même quand Strapi répond vite. Le ressenti « ça rame » vient + en partie de là, indépendamment du poids des images. +2. Le HTML initial **ne contient aucune ``** → le navigateur ne peut pas + préfetcher les visuels pendant le parsing HTML. +3. Pas de cache Next (`fetch` côté client = cache navigateur uniquement, + pas le data cache de Next). + +> **Constat n° 3 :** migrer ces pages en **Server Components** (fetch dans le +> composant async + `revalidate: 60`) résoudrait à la fois le ressenti de +> lenteur **et** la latence images, puisque les `` seraient dans le HTML +> initial — le navigateur lance les requêtes images en parallèle du JS. + +## 4. Mode `dev` : impact réel + +L'utilisateur souhaite **rester en dev pour le moment** — c'est noté. Voici +ce que ça coûte vraiment, du plus marquant au moins marquant. + +### 4.1 Compression HTTP désactivée explicitement (Next) + +```9:12:next.config.ts +const nextConfig = { + reactStrictMode: true, + compress: false, + trailingSlash: false, +``` + +`compress: false` désactive **gzip/brotli** côté Next — y compris en +production. Pour du JSON Strapi de 50 KB ou un bundle JS de 500 KB, c'est un +facteur 4 à 8 sur le poids transféré. **À retirer ou passer à `true` même +en dev.** + +### 4.2 `next dev --turbopack` + +Coût : pas de minification, pas de tree-shaking, sourcemaps, modules +instrumentés HMR. **Impact net :** bundle JS ~3-5× plus lourd qu'en prod, +mais ça ne touche pas les images. Visible surtout au premier chargement de +l'app. + +### 4.3 `strapi develop` + +- watcher tsc + reload admin sur chaque écriture ; +- logs verbeux ; +- pas de cache compilé — chaque requête ré-exécute le pipeline middlewares + complet. + +**Impact sur les images :** marginal (sharp redimensionne et met en cache +les variantes au premier upload, pas à chaque requête). Strapi dev est +**lent à démarrer**, pas lent à servir un fichier statique. + +### 4.4 Aucun cache HTTP côté Strapi + +Le middleware `strapi::public` sert `cmsbackend/public/uploads/` sans +`Cache-Control` long terme. Chaque revisite re-télécharge potentiellement +l'image (selon ETag). C'est aggravé en dev car les builds suivants +invalidant tout. **Voir `cmsbackend/config/middlewares.ts`** : + +```19:26:cmsbackend/config/middlewares.ts + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], + credentials: true, + }, + }, + 'strapi::poweredBy', + 'strapi::query', + 'strapi::body', + 'strapi::session', + 'strapi::favicon', + 'strapi::public', +]; +``` + +> **Constat n° 4 :** en dev, l'impact « gros » est `compress: false`. Le +> reste du dev mode est inconfortable au démarrage mais n'explique pas la +> lenteur image perçue. + +## 5. Autres pistes détectées en cours d'audit + +### 5.1 `app/assets/images/` pèse 992 MB dans le repo + +``` +339 fichiers — 992,4 MB + .png : 137 fichiers — 871,7 MB (médianes ~3-10 MB par fichier) + .webp: 168 fichiers — 73,9 MB + .jpg : 33 fichiers — 46,8 MB +``` + +Dossier hérité de l'époque où les images étaient bundlées dans le code +Next. Aujourd'hui les pages tirent depuis Strapi, donc **ces PNG sont +probablement morts** mais alourdissent : `git clone`, sauvegardes, indexation +IDE, et potentiellement `next build` s'ils sont importés depuis un fichier +encore référencé. À auditer (`grep` sur les imports) puis purger ou archiver. + +### 5.2 Pas de `link rel="preload"` pour le portrait hero + +`app/page.tsx` (home) affiche un portrait via `` après fetch +client-side. Sur la home, c'est l'image principale au-dessus de la ligne de +flottaison ; un preload (ou un Server Component + `next/image priority`) +ferait gagner ~200-500 ms de TTI sur le LCP. + +### 5.3 Pas de hint réseau vers `api.fernandgrascalvet.com` + +Aucun `` +dans `app/layout.tsx`. Le premier round-trip image en prod paye le DNS + +TLS handshake en série ; un preconnect le déclenche pendant le parsing +HTML. + +### 5.4 Wallpaper OK, alternatives mortes + +`app/assets/images/wallpapersite_resultat.webp` ≈ **620 KB** — déjà WebP, +correct pour un fond plein écran. Le `wallpapersite.png` à côté pèse +6,8 MB et n'est plus utilisé : à supprimer. + +## 6. Plan d'action proposé (du plus rentable au plus structurel) + +Tri par ratio gain / effort. À discuter avant exécution. + +### Quick wins (≤ 1 h, gros gains) + +- [ ] **Lot A — Lire les variantes Strapi côté Next.** + Modifier les 6 emplacements `` pour préférer + `img.formats?.medium?.url ?? img.formats?.small?.url ?? img.url`. + Ajouter une fonction utilitaire `pickStrapiImage(picture, "card" | "thumbnail" | "full")` + dans `app/utils/`. **Gain attendu :** ÷ 5 à ÷ 10 sur le poids des grilles + de portfolio/compétences. Aucune perte qualité (les variantes sont + dimensionnées par Strapi à partir des originaux). +- [ ] **Lot B — Activer la compression Next.** + Passer `compress: false` → `compress: true` dans `next.config.ts`. + **Gain attendu :** ÷ 4 sur le JSON Strapi et le HTML. +- [ ] **Lot C — `preconnect` API Strapi.** + Ajouter `` + dans `app/layout.tsx`. **Gain attendu :** ~150-300 ms sur le TTFB des + images en prod, négligeable en local. + +### Lots moyens (1-3 h chacun) + +- [ ] **Lot D — Script d'inventaire & conversion WebP.** + Nouveau script `strapi_extraction/audit-images.js` qui : + 1. parcourt `cmsbackend/public/uploads/` ; + 2. identifie les originaux non-WebP > seuil (1 MB par défaut) ; + 3. classe par section (en croisant avec + `extract/raw/projects-raw.json`, `competences-raw.json`, + `homepages-raw.json`) → produit + `strapi_extraction/extract/images-by-section/
/.json` ; + 4. exporte un rapport `images-audit.md` (top n par poids, par section, + fichiers orphelins). + **Pas de conversion automatique au premier passage** — juste l'inventaire, + pour que l'utilisateur puisse vérifier la classification avant action. + +- [ ] **Lot E — Conversion + ré-upload contrôlé.** + Une fois l'inventaire validé : second script + `strapi_extraction/convert-and-reupload.js` qui : + 1. convertit les fichiers ciblés via `sharp` (qualité 80, conserve les + dimensions) → écrit dans `extract/converted/` ; + 2. ré-upload via l'API REST Strapi (`POST /api/upload`) avec + `ref`/`refId`/`field` pour rattacher à la bonne entité ; + 3. supprime l'ancien original via `DELETE /upload/files/:id` après + vérification. + **Réversibilité :** le script garde les originaux dans `extract/backup/` + jusqu'à validation manuelle. + +- [ ] **Lot F — Migration `` → `next/image`.** + Remplacer les 6 occurrences. Mettre à jour `next.config.ts` : + `domains` → `remotePatterns`, ajouter + `formats: ['image/avif', 'image/webp']`, définir `deviceSizes` cohérents + avec les breakpoints Tailwind. Ajouter `priority` au portrait hero, + `placeholder="blur"` (avec `blurDataURL` provenant de `formats.thumbnail`). + +### Lots structurels (½ journée +) + +- [ ] **Lot G — Server Components pour `/portfolio`, `/competences` et `/`.** + Convertir les pages en composants async serveur, déplacer le `useEffect` + vers un sous-composant client uniquement pour l'interactivité (carousels, + modal). Bénéfices : HTML initial complet, fetch caché serveur, pas de + spinner au premier paint. Compatible avec Lot F (`next/image priority`). + +- [ ] **Lot H — Plugin Strapi pour conversion WebP à l'upload.** + Configurer `@strapi/provider-upload-local` (ou un plugin custom) pour + forcer la conversion WebP des nouveaux uploads via sharp. Évite la + ré-introduction de PNG bruts à l'avenir. **Hors scope du dev local + immédiat — à faire avant de remettre en prod.** + +- [ ] **Lot I — Nettoyage `app/assets/images/`.** + Identifier les fichiers encore importés (`grep` sur les chemins). + Archiver (zip externe) le reste. Gain : ~900 MB sur le repo. + +## 7. Métriques avant/après à capturer + +À la fin de chaque lot, mesurer : + +- Poids total transféré sur `/portfolio` (DevTools → Network → "Img"), en + cache vide et en cache plein ; +- LCP et CLS via Lighthouse en mode mobile/3G simulé ; +- Time-to-interactive sur Firefox + Safari (les navigateurs cibles + signalés comme problématiques). + +Stocker les captures dans `docs-site-interne/captures/perf/` avec le +nom `-.webp`. + +## 8. Ce que ce document **ne** fait pas (encore) + +- Aucune ligne de code modifiée — c'est un audit. +- Le script `strapi_extraction/audit-images.js` (Lot D) reste à écrire. +- Les conversions WebP (Lot E) restent à faire. + +À la prochaine itération : créer le script d'audit images en s'inspirant +de la structure de `strapi_extraction/extract-api-data.js` (même style de +log, même répertoire `extract/`, même résumé JSON final). + +## 9. Liens internes + +- Pipeline d'extraction Strapi existant : + [`06-strapi-extraction.md`](./06-strapi-extraction.md) +- Architecture globale & ports : + [`01-architecture.md`](./01-architecture.md) +- Doc front Next : + [`02-frontend-next.md`](./02-frontend-next.md) +- État courant du chantier : + [`etat-actuel.md`](./etat-actuel.md) +- Roadmap : + [`feuille-de-route.md`](./feuille-de-route.md)