Performances React
Modèles d'optimisation des performances pour React 18/19 et Next.js, adaptés de Vercel Labs react-best-practices (MIT, v1.0.0). Ce skill organise les règles par priorité et fournit un arbre de décision pour la revue de code active et le refactoring.
Quand activer
- Écrire ou réviser du code React/Next.js pour les performances
- Diagnostiquer des chargements de page lents, des interactions lentes ou une utilisation élevée du CPU côté client
- Auditer la taille du bundle ou les régressions Lighthouse Core Web Vitals
- Supprimer les waterfalls dans les Server Components / routes API
- Réduire les re-rendus côté client
- Optimiser les longues listes, les animations ou l'hydratation
- Auditer les choix d'optimisation dans les PRs qui touchent
app/,pages/,components/ou les couches de données
Index de priorité
| Priorité | Catégorie | Préfixe | Quand cela compte |
|---|---|---|---|
| 1 — CRITIQUE | Éliminer les waterfalls | async- | Chaque fois que await est suivi d'un await indépendant |
| 2 — CRITIQUE | Optimisation de la taille du bundle | bundle- | Chargement initial JS, imports au niveau des routes, bibliothèques tierces |
| 3 — ÉLEVÉE | Performances côté serveur | server- | RSC, Server Actions, routes API, SSR |
| 4 — MOYENNE-ÉLEVÉE | Récupération de données côté client | client- | SWR / TanStack Query / fetch brut dans les hooks |
| 5 — MOYENNE | Optimisation des re-rendus | rerender- | Mises à jour d'état à haute fréquence, fan-out parent-enfant |
| 6 — MOYENNE | Performances de rendu | rendering- | Longues listes, animations, hydratation |
| 7 — FAIBLE-MOYENNE | Performances JavaScript | js- | Boucles chaudes, allocations fréquentes |
| 8 — FAIBLE | Modèles avancés | advanced- | Intégration effet-événement, refs stables |
1. Éliminer les waterfalls (CRITIQUE)
« Les waterfalls sont le tueur de performances numéro un » — chaque
awaitséquentiel ajoute la latence réseau complète.
Conditions peu coûteuses avant await
Vérifiez les conditions synchrones (props, env, drapeaux codés en dur) avant d'attendre des données distantes.
// INCORRECT
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// CORRECT — court-circuit sur condition synchrone peu coûteuse en premier
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}
Reporter les awaits jusqu'à leur utilisation
Déplacez await dans la branche qui l'utilise.
// INCORRECT — attend avant de décider si les données sont nécessaires
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// CORRECT
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);
Promise.all pour les tâches indépendantes
// INCORRECT — séquentiel
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// CORRECT — parallèle
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);
Dépendances partielles — commencer tôt, attendre tard
// CORRECT — lancez toutes les promesses, attendez seulement quand chaque résultat est nécessaire
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);
Suspense pour le streaming
Poussez les limites <Suspense> près des données afin que la page affiche ce qu'elle peut pendant que les sous-arbres plus lents arrivent en streaming. Le compromis : décalage de mise en page lorsque le contenu arrive — réservez de l'espace (skeleton ou min-height).
Server Components : parallélisation par composition
// INCORRECT — les awaits frères s'exécutent séquentiellement dans un seul composant
export default async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// CORRECT — divisez en enfants, React les exécute en parallèle
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}
2. Optimisation de la taille du bundle (CRITIQUE)
Imports directs, pas de barils
Les fichiers baril index.ts forcent le bundler à parcourir tout le graphe de modules même si l'élimination du code mort supprime la plupart de son contenu. Les imports directs permettent d'économiser 200 à 800 ms de JS de chargement initial dans de nombreuses applications réelles.
// INCORRECT
import { Button, Card, Modal } from "@/components";
// CORRECT
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";
Next.js 13.5+ dispose d'Optimize Package Imports qui automatise cela pour les packages répertoriés — utilisez-le ; les imports directs manuels sont toujours nécessaires pour les bibliothèques non répertoriées.
Chemins analysables statiquement
// INCORRECT — empêche l'analyse du bundler/trace
const mod = await import(`./pages/${name}`);
// CORRECT — explicite par branche
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
Imports dynamiques pour les composants lourds
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // lorsque client uniquement
});
Différer les scripts tiers
Chargez les analytics, la journalisation, les widgets d'assistance APRÈS l'hydratation. Utilisez next/script avec strategy="afterInteractive" (par défaut) ou "lazyOnload".
Chargement conditionnel de modules
if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}
Précharger au survol/au focus
Déclenchez <link rel="preload"> ou import() au survol pour que le bundle soit dans le cache au moment où l'utilisateur clique.
3. Performances côté serveur (ÉLEVÉE)
Authentifier les Server Actions comme les routes API
Chaque fonction "use server" est un point de terminaison public. Authentifiez ET autorisez à l'intérieur de l'action — ne vous fiez jamais au contrôle du Client Component appelant.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Non autorisé");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Interdit");
}
await db.user.delete({ where: { id: targetId } });
}
React.cache() pour la déduplication par requête
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache déduplique au sein d'une seule requête. Appeler getUser("1") depuis trois Server Components dans le même rendu = une requête DB.
Cache LRU pour les données inter-requêtes
Pour les données qui NE changent PAS par requête (configuration, tables de correspondance), mettez en cache en dehors de React avec un cache LRU ou unstable_cache.
Éviter la sérialisation en double dans les props RSC
Lorsqu'un Server Component rend les mêmes données dans plusieurs Client Components, les données sont sérialisées une fois par consommateur. Remontez le Client Component et passez des enfants.
Déplacer les E/S statiques vers la portée du module
// CORRECT — s'exécute une fois au chargement du module
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}
Pas d'état mutable au niveau du module dans RSC/SSR
L'état du module sur le serveur est partagé entre toutes les requêtes — une condition de concurrence entre les utilisateurs. Utilisez un stockage limité à la requête (headers(), cookies(), contexte asynchrone) à la place.
Minimiser les données passées aux Client Components
Sérialisez uniquement ce dont le Client a besoin. Supprimez les champs, paginez, projetez les colonnes au niveau de la couche DB.
Paralléliser les fetchs imbriqués avec Promise.all par élément
const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);
Utiliser after() pour les tâches non bloquantes
after() de Next.js 15 exécute le travail après l'envoi de la réponse — journalisation, réchauffement du cache, analytics.
import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}
4. Récupération de données côté client (MOYENNE-ÉLEVÉE)
SWR / TanStack Query pour la déduplication
Plusieurs composants appelant useUser(id) doivent partager une seule requête réseau et une seule entrée de cache. Utilisez SWR ou TanStack Query — n'implémentez jamais votre propre useEffect + fetch pour des données partagées.
Dédupliquer les écouteurs d'événements globaux
// INCORRECT — chaque composant ajoute le sien
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// CORRECT — écouteur partagé unique via un hook + sujet global
const useScroll = createScrollHook(); // sujet singleton sous le capot
Écouteurs passifs pour le scroll
window.addEventListener("scroll", handler, { passive: true });
Améliore la fluidité du défilement ; l'écouteur ne peut pas appeler preventDefault().
localStorage : version + minimiser
- Stockez toujours un champ
version; incrémentez-le lors d'un changement de schéma et migrez ou jetez les anciennes données - Gardez les charges utiles petites —
localStorageest synchrone et bloque le thread principal
5. Optimisation des re-rendus (MOYENNE)
Ne pas s'abonner à un état utilisé uniquement dans les callbacks
// INCORRECT — se re-rend à chaque fois que count change
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// CORRECT — lu une fois à l'appel
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Extraire les travaux coûteux dans des composants mémorisés
// CORRECT — l'enfant se re-rend uniquement lorsque `items` change
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
Remonter les props non primitives par défaut
// INCORRECT — nouveau tableau à chaque rendu, ce qui casse memo
<List items={items ?? []} />
// CORRECT
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
Dépendances primitives dans les effets
// INCORRECT — nouvel identifiant d'objet à chaque rendu
useEffect(() => {}, [{ id, name }]);
// CORRECT — primitives
useEffect(() => {}, [id, name]);
S'abonner à des booléens dérivés, pas à des valeurs brutes
// INCORRECT — se re-rend pour tout changement de panier
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRECT — se re-rend uniquement lorsque le booléen bascule
const hasItems = useStore((s) => s.cart.length > 0);
Dériver pendant le rendu, jamais via useEffect
// INCORRECT
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// CORRECT
const full = `${first} ${last}`;
setState fonctionnel pour des callbacks stables
// CORRECT
const increment = useCallback(() => setCount((c) => c + 1), []);
Initialiseur d'état paresseux pour les valeurs coûteuses
const [tree] = useState(() => parseTree(largeInput));
Éviter memo pour les primitives simples
useMemo(() => x + 1, [x]) est de la surcharge. Memo est utile pour l'identité d'objet et les calculs coûteux.
Diviser les hooks avec des dépendances indépendantes
// INCORRECT — les deux sélecteurs se réexécutent si l'une ou l'autre source change
const { a, b } = useSomething(source1, source2);
// CORRECT
const a = useA(source1);
const b = useB(source2);
Déplacer la logique d'interaction dans les gestionnaires d'événements
Les gestionnaires d'événements s'exécutent uniquement sur l'action de l'utilisateur — useEffect se réexécute à chaque changement de dépendance.
startTransition pour les mises à jour non urgentes
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
useDeferredValue pour les rendus coûteux
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
useRef pour les valeurs transitoires fréquentes
Pour les valeurs qui changent souvent mais ne doivent pas déclencher de re-rendu (horodatages, dernière clé, accumulateurs).
Ne pas définir de composants à l'intérieur de composants
// INCORRECT — Inner est un nouveau composant à chaque rendu d'Outer
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Chaque rendu crée un nouveau type Inner, ce qui empêche la réconciliation et démonte les enfants.
6. Performances de rendu (MOYENNE)
Animer le wrapper, pas le SVG
Transformer une <div> wrapper autour d'un SVG est accéléré par le GPU ; transformer le SVG lui-même déclenche le paint.
content-visibility: auto pour les longues listes
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
Le navigateur ignore le rendu hors écran — un gain majeur pour les listes avec des centaines de lignes.
Remonter le JSX statique
const STATIC_HEADER = <h1>Titre</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG : réduire la précision des coordonnées
d="M10.123456,20.654321" → d="M10.12,20.65". Chaque chiffre coûte des octets ; la différence visuelle est inférieure au pixel.
Pas de scintillement d'hydratation via script en ligne
Pour les valeurs nécessaires avant l'hydratation (thème, locale), intégrez un <script> qui définit document.documentElement.dataset.* avant que React ne monte.
Supprimer les incohérences d'hydratation attendues de manière étroite
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Utilisez UNIQUEMENT pour les nœuds feuilles connus pour diverger — jamais sur un arbre contenant d'autres enfants.
<Activity> pour afficher/masquer au lieu de monter/démonter
React 19 <Activity mode="visible|hidden"> maintient l'état et les effets de l'arbre montés mais masqués — moins coûteux que le démontage/remontage pour les onglets et les accordéons.
Opérateur ternaire plutôt que && pour le rendu conditionnel
// INCORRECT — `0` s'affiche comme un nœud de texte
{count && <Badge>{count}</Badge>}
// CORRECT
{count > 0 ? <Badge>{count}</Badge> : null}
useTransition pour les états de chargement
Associez startTransition à l'action ; React affiche l'interface utilisateur précédente comme isPending pendant que l'état suivant est calculé.
Indices de ressources React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")
…
[tronqué pour la traduction]