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 :
| Phase | Objectif | Durée max |
|---|---|---|
| Response | Réagir à une interaction | < 100ms (50ms idéal) |
| Animation | 60 fps (16ms par frame) | < 10ms par frame (hors composition) |
| Idle | Maximiser les périodes inactives | Regrouper les tâches différées |
| Load | Charger 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
| Format | Compression | Qualité | Support |
|---|---|---|---|
| JPEG | Modéré | Bon | Universel |
| PNG | Aucune (lossless) | Parfaite | Universel |
| WebP | ~30% plus petit que JPEG | Comparable | 95%+ |
| AVIF | ~50% plus petit que JPEG | Supérieure | 80%+ |
| SVG | Variable | Vectorielle | Universel |
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ère | Webpack | Vite | Turbopack |
|---|---|---|---|
| Dev server | Lent (rebundle) | Instant (ESM natif) | Très rapide (Rust) |
| Build | Lent | Rapide (Rollup) | Très rapide (Rust) |
| Configuration | Complexe | Simple | Simple |
| Écosystème | Très mature | Mature | Début |
| HMR | Lent | Instant | Instant |
| Tree Shaking | Bon | Excellent | Excellent |
| Adoption | Legacy | Moderne | É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
- Performance : LCP, TBT, CLS
- Accessibility : contrastes, ARIA
- Best Practices : HTTPS, sécurité
- SEO : meta, sémantique
- 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: autopour 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