devsite/docs-site-interne/09-performances-images.md
2026-04-28 10:33:07 +02:00

347 lines
14 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.

# 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
<img
src={firstImage.url}
alt={firstImage.alt}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
loading="lazy"
/>
```
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 `<img>` 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 `<img>`** → 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 `<img>` 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 `<img>` 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 `<link rel="preconnect" href="https://api.fernandgrascalvet.com">`
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 `<img src={img.url}>` 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 `<link rel="preconnect" href="https://api.fernandgrascalvet.com" crossOrigin="" />`
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/<section>/<slug>.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 `<img>` → `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 `<lot>-<avant|apres>.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)