MFormations
Modern Frontend Engineering

Chapitre 11

Chapitre 11 — Performance

> Optimiser les performances d'une application front-end moderne, des métriques aux stratégies avancées.

Performance — Cours complet

1. Pourquoi la performance ?

La performance impacte directement :

  • UX : 53% des utilisateurs quittent un site qui met plus de 3 secondes à charger
  • SEO : Google utilise les Core Web Vitals comme facteur de ranking
  • Conversion : 1 seconde de latence = -7% de conversions (Amazon)
  • Accessibilité : les utilisateurs à connexion lente sont souvent dans des zones mal desservies
  • Coût : moins de bande passante = coût serveur réduit
  • Batterie : moins de CPU = batterie préservée (mobile)

Core Web Vitals

Les Core Web Vitals sont les métriques que Google considère comme essentielles :

Core Web Vitals
├── LCP (Largest Contentful Paint) — Chargement perçu
│   ├── Good: ≤ 2.5s
│   ├── Needs Improvement: ≤ 4.0s
│   └── Poor: > 4.0s
├── INP (Interaction to Next Paint) — Interactivité
│   ├── Good: ≤ 200ms
│   ├── Needs Improvement: ≤ 500ms
│   └── Poor: > 500ms
└── CLS (Cumulative Layout Shift) — Stabilité visuelle
    ├── Good: ≤ 0.1
    ├── Needs Improvement: ≤ 0.25
    └── Poor: > 0.25

2. Métriques détaillées

FCP (First Contentful Paint)

Quand le premier élément (texte, image, canvas) est affiché.

  • Bon : ≤ 1.8s
  • Mauvais : > 3.0s

TTFB (Time To First Byte)

Temps entre la requête et la réception du premier octet.

  • Bon : ≤ 0.8s
  • Mauvais : > 1.8s

TBT (Total Blocking Time)

Somme des périodes où le thread principal est bloqué (par du JS long).

  • Bon : ≤ 200ms
  • Mauvais : > 600ms

SI (Speed Index)

Vitesse à laquelle le contenu visible est affiché.

  • Bon : ≤ 3.4s
  • Mauvais : > 5.8s

3. RAIL Model

Modèle centré sur l'utilisateur :

PhaseObjectifDurée max
ResponseRéagir à une interaction< 100ms (50ms idéal)
Animation60 fps (16ms par frame)< 10ms par frame (hors composition)
IdleMaximiser les périodes inactivesRegrouper les tâches différées
LoadCharger le contenu< 1s pour la première interaction
// Exemple : Animation à 60fps
function animate() {
  const start = performance.now();
  requestAnimationFrame(() => {
    // Travail < 10ms
    const elapsed = performance.now() - start;
    if (elapsed > 16) console.warn('Frame dropped!');
  });
}

4. Critical Rendering Path

Le chemin que le navigateur parcourt pour afficher une page :

HTML → DOM
CSS → CSSOM
DOM + CSSOM → Render Tree → Layout → Paint → Composite

Optimiser HTML

<!-- ✅ Charset en premier -->
<meta charset="UTF-8" />
<!-- ✅ Viewport critical -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- ✅ CSS critique inline -->
<style>
  header { background: #333; color: white; }
  .hero { font-size: clamp(2rem, 5vw, 4rem); }
</style>

Optimiser CSS

<!-- ✅ CSS critique inline, le reste en différé -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
/* ✅ Media queries pour le chargement conditionnel */
<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="mobile.css" media="(max-width: 640px)" />

Optimiser JS

<!-- ✅ async / defer pour ne pas bloquer le rendu -->
<script async src="analytics.js"></script>
<script defer src="app.js"></script>

<!-- ✅ ES modules chargés en différé -->
<script type="module" src="main.js"></script>

5. Optimisation des images

Formats modernes

FormatCompressionQualitéSupport
JPEGModéréBonUniversel
PNGAucune (lossless)ParfaiteUniversel
WebP~30% plus petit que JPEGComparable95%+
AVIF~50% plus petit que JPEGSupérieure80%+
SVGVariableVectorielleUniversel

Lazy loading

<!-- ✅ Native lazy loading -->
<img src="photo.webp" loading="lazy" alt="Description" />
<iframe src="widget.html" loading="lazy" title="Widget"></iframe>
// ✅ Intersection Observer pour le lazy loading avancé
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));

Images responsives

<img
  src="photo-800.webp"
  srcset="
    photo-400.webp 400w,
    photo-800.webp 800w,
    photo-1200.webp 1200w
  "
  sizes="(max-width: 640px) 100vw, 50vw"
  alt="Description"
/>

Optimisation automatique

// next/image (Next.js)
import Image from 'next/image';

<Image src="/hero.jpg" width={1200} height={600} alt="Hero" priority />

6. Code Splitting

Dynamic Import

// ✅ Chargement à la demande
const { format } = await import('date-fns');

React.lazy + Suspense

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyComponent />
    </Suspense>
  );
}

Route-level splitting

// react-router v7
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

<Routes>
  <Route path="/dashboard" element={
    <Suspense fallback={<PageSkeleton />}><Dashboard /></Suspense>
  } />
  <Route path="/settings" element={
    <Suspense fallback={<PageSkeleton />}><Settings /></Suspense>
  } />
</Routes>

7. Bundlers

Webpack vs Vite vs Turbopack

CritèreWebpackViteTurbopack
Dev serverLent (rebundle)Instant (ESM natif)Très rapide (Rust)
BuildLentRapide (Rollup)Très rapide (Rust)
ConfigurationComplexeSimpleSimple
ÉcosystèmeTrès matureMatureDébut
HMRLentInstantInstant
Tree ShakingBonExcellentExcellent
AdoptionLegacyModerneÉmergente
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['date-fns', 'lodash-es'],
        },
      },
    },
  },
});

8. Tree Shaking et Dead Code Elimination

Tree Shaking

// ✅ Import nommé → tree-shakable
import { format } from 'date-fns';

// ❌ Import entier → pas de tree shaking
import dateFns from 'date-fns';

Dead Code Elimination

// ✅ Le code mort est éliminé à la build
if (process.env.NODE_ENV === 'development') {
  console.log('Debug mode'); // Éliminé en production
}

Configuration

{
  "sideEffects": false,
  "exports": {
    "./Button": "./src/components/Button.tsx",
    "./Input": "./src/components/Input.tsx"
  }
}

9. Caching

HTTP Cache

# Cache immutable pour les assets versionnés
location /assets/ {
  expires 1y;
  add_header Cache-Control "public, immutable, max-age=31536000";
}

# Cache court pour les pages HTML
location / {
  add_header Cache-Control "no-cache, must-revalidate";
}

Service Worker (Workbox)

// service-worker.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';

precacheAndRoute(self.__WB_MANIFEST);

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'images' })
);

registerRoute(
  ({ url }) => url.pathname.startsWith('/api'),
  new NetworkFirst({ cacheName: 'api' })
);

Cache API

// Cache manuel
const cache = await caches.open('app-v1');
await cache.put('/api/data', new Response(JSON.stringify(data)));
const cached = await cache.match('/api/data');

10. Lighthouse

Audits

npx lighthouse https://example.com --view
npx lighthouse https://example.com --output=json --output-path=./report.json

Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: treosh/lighthouse-ci-action@v10
        with:
          urls: |
            https://example.com
            https://example.com/about
          budgetPath: ./lighthouse-budget.json

Amélioration continue

  1. Performance : LCP, TBT, CLS
  2. Accessibility : contrastes, ARIA
  3. Best Practices : HTTPS, sécurité
  4. SEO : meta, sémantique
  5. PWA : service worker, manifest

11. Performance Budgets

{
  "performance-budget": {
    "timings": [
      { "metric": "interactive", "budget": 3000 },
      { "metric": "first-meaningful-paint", "budget": 1000 }
    ],
    "resource-sizes": [
      { "resourceType": "total", "budget": 500000 },
      { "resourceType": "script", "budget": 200000 },
      { "resourceType": "image", "budget": 150000 },
      { "resourceType": "font", "budget": 50000 }
    ],
    "resource-counts": [
      { "resourceType": "total", "budget": 50 },
      { "resourceType": "script", "budget": 10 }
    ]
  }
}

Outils de vérification

  • Lighthouse CI (budgetPath)
  • webpack-bundle-analyzer
  • bundlesize (GitHub Checks)
  • size-limit

12. Web Vitals en production

RUM (Real User Monitoring)

import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  navigator.sendBeacon('/analytics', JSON.stringify(metric));
}

onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);

CrUX (Chrome User Experience Report)

Données agrégées de Google sur les performances réelles des pages.

-- CrUX via BigQuery
SELECT
  origin,
  AVIF(lcp.good_proportion) AS lcp_good_pct
FROM `chrome-ux-report.all.202606`
WHERE origin = 'https://example.com';

13. Core Web Vitals — Stratégies d'optimisation

LCP (Largest Contentful Paint)

<!-- ✅ Précharger l'image LCP -->
<link rel="preload" href="/hero.webp" as="image" />

<!-- ✅ Optimiser le serveur (TTFB) -->
<!-- CDN, compression, cache -->

<!-- ✅ Éviter les bloqueurs de rendu -->
<script defer src="app.js"></script>

CLS (Cumulative Layout Shift)

/* ✅ Dimensions explicites pour les images */
img, video, iframe {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* ✅ Réserver l'espace pour les polices */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter.woff2') format('woff2');
  font-display: swap;
  size-adjust: 100%;
}

/* ✅ Éviter les insertions DOM après le rendu */
/* Ne pas injecter de contenu sans espace réservé */

INP (Interaction to Next Paint)

// ✅ Éviter les long tasks
function processLargeArray(items) {
  const chunkSize = 50;
  let index = 0;

  function processChunk() {
    const end = Math.min(index + chunkSize, items.length);
    for (let i = index; i < end; i++) {
      processItem(items[i]);
    }
    index = end;
    if (index < items.length) {
      requestIdleCallback(processChunk);
    }
  }

  requestIdleCallback(processChunk);
}

14. Patterns de performance avancés

Virtualisation (react-window)

import { FixedSizeList } from 'react-window';

function VirtualList({ items }: { items: string[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>{items[index]}</div>
  );

  return (
    <FixedSizeList height={600} itemCount={items.length} itemSize={50}>
      {Row}
    </FixedSizeList>
  );
}

Streaming (React 19)

async function SlowComponent() {
  const data = await fetchData(); // Streaming SSR
  return <div>{data}</div>;
}

Islands Architecture (Astro)

<!-- Content statique (HTML pur, pas de JS) -->
<header>
  <nav>
    <a href="/">Home</a>
  </nav>
</header>

<!-- Île interactive (hydratée séparément) -->
<Island client:load>
  <Counter />
</Island>

15. JavaScript Performance

Memory Leaks

// ❌ Event listener non nettoyé
useEffect(() => {
  window.addEventListener('resize', handleResize);
  // ❌ Pas de cleanup !
}, []);

// ✅ Cleanup du listener
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Bundle Analysis

npx vite-bundle-visualizer
npx webpack-bundle-analyzer dist/stats.json

React DevTools Profiler

import { Profiler } from 'react';

function onRender(id, phase, actualDuration) {
  console.log(`${id} (${phase}): ${actualDuration}ms`);
}

<Profiler id="Dashboard" onRender={onRender}>
  <Dashboard />
</Profiler>

16. Checklist d'optimisation

Loading

  • Images optimisées (WebP/AVIF)
  • Lazy loading sur les images hors écran
  • Code splitting par route
  • CSS critique inline
  • JS différé (defer/async)
  • Polices optimisées (WOFF2, font-display: swap)
  • Preconnect vers les origines tierces
  • Preload des ressources critiques

Rendering

  • Éviter les layout shifts (dimensions explicites)
  • Respecter 60fps pour les animations
  • Utiliser content-visibility: auto pour le contenu hors écran
  • Éviter les long tasks (> 50ms)

Network

  • HTTP/2 ou HTTP/3
  • Compression Brotli
  • CDN
  • Cache headers optimisés
  • Service Worker pour offline

Monitoring

  • RUM avec web-vitals
  • Lighthouse CI
  • Performance budgets
  • Alerting sur dégradation