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

14 KiB
Raw Blame History

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) :

                    <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 :

  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)

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 :

      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.

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.webp620 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: falsecompress: 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 : domainsremotePatterns, 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