Performances React
Patrons 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 et le refactoring en cours.
Quand l'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 des Core Web Vitals Lighthouse
- Supprimer les waterfalls dans les Server Components / routes API
- Réduire les re-rendus côté client
- Optimiser les listes longues, les animations ou l'hydratation
- Auditer les choix d'optimisation dans les PR touchant
app/,pages/,components/ou les couches de données
Index de priorité
| Priorité | Catégorie | Préfixe | Quand ça compte |
|---|---|---|---|
| 1 — CRITIQUE | Éliminer les waterfalls | async- | Chaque fois qu'un await est suivi d'un await indépendant |
| 2 — CRITIQUE | Optimisation de la taille du bundle | bundle- | JS de premier chargement, imports par route, bibliothèques tierces |
| 3 — HAUTE | Performances côté serveur | server- | RSC, Server Actions, routes API, SSR |
| 4 — MOYENNE-HAUTE | 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- | Listes longues, animations, hydratation |
| 7 — BASSE-MOYENNE | Performances JavaScript | js- | Boucles chaudes, allocations fréquentes |
| 8 — BASSE | Patrons avancés | advanced- | Intégration effet-événement, références stables |
1. Éliminer les waterfalls (CRITIQUE)
"Les waterfalls sont le tueur de performances n°1" — chaque
awaitséquentiel ajoute toute la latence réseau.
Conditions peu coûteuses avant l'await
Vérifiez les conditions synchrones (props, env, flags 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-circuite d'abord sur une condition synchrone peu coûteuse
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 — commencez tôt, attendez tard
// CORRECT — lance toutes les promesses, attend chaque résultat seulement quand 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
Placez 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 : un décalage de mise en page lorsque le contenu arrive — réservez 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 le tree-shaking supprime la majeure partie. Les imports directs économisent 200 à 800 ms de JS de premier chargement 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+ propose Optimize Package Imports qui automatise cela pour les packages listés — utilisez-le ; les imports directs manuels restent nécessaires pour les bibliothèques non listées.
Chemins statiquement analysables
// 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, // quand côté client uniquement
});
Reporter les scripts tiers
Chargez les outils d'analyse, de journalisation et 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échargement au survol/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 (HAUTE)
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 garde-fou du Client Component appelant.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
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 seule 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.
Remonter les E/S statiques au niveau 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 utilisateurs. Utilisez plutôt un stockage à portée de requête (headers(), cookies(), contexte asynchrone).
Minimiser les données transmises aux Client Components
Sérialisez uniquement ce dont le Client a besoin. Supprimez les champs, paginez, projetez les colonnes au niveau de la base de données.
Paralléliser les récupérations imbriquées 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, préchauffage du cache, analyses.
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-HAUTE)
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 — ne créez 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 — un seul écouteur partagé via un hook + un sujet global
const useScroll = createScrollHook(); // singleton sous le capot
Écouteurs passifs pour le défilement
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 en cas de changement de schéma et migrez ou supprimez les anciennes données - Gardez les charges utiles petites —
localStorageest synchrone et bloque le thread principal
5. Optimisation des re-rendus (MOYENNE)
Ne vous abonnez pas à un état utilisé uniquement dans les callbacks
// INCORRECT — re-rend à chaque changement de count
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// CORRECT — lu une fois lors de l'appel
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Extrayez le travail coûteux dans des composants mémoïsés
// CORRECT — l'enfant se re-rend uniquement quand `items` change
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
Remontez 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 objet à chaque rendu
useEffect(() => {}, [{ id, name }]);
// CORRECT — primitives
useEffect(() => {}, [id, name]);
Abonnez-vous à des booléens dérivés, pas à des valeurs brutes
// INCORRECT — re-rend pour tout changement du panier
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRECT — re-rend uniquement quand l'état vide/vide change
const hasItems = useStore((s) => s.cart.length > 0);
Dérivez 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 les callbacks stables
// CORRECT
const increment = useCallback(() => setCount((c) => c + 1), []);
Initialisateur d'état paresseux pour les valeurs coûteuses
const [tree] = useState(() => parseTree(largeInput));
Évitez memo pour les primitives simples
useMemo(() => x + 1, [x]) est une surcharge. Memo est utile pour l'identité d'objet et les calculs coûteux.
Séparez 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éplacez 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 fois que les dépendances changent.
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 fréquentes transitoires
Pour les valeurs qui changent souvent mais ne doivent pas déclencher de re-rendu (horodatages, dernière touche, accumulateurs).
Ne définissez pas de composants à l'intérieur des 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)
Animez le wrapper, pas le SVG
Transformer un <div> wrapper autour d'un SVG est accéléré par le GPU ; transformer le SVG lui-même déclenche un repaint.
content-visibility: auto pour les listes longues
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
Le navigateur saute le rendu hors écran — un gain majeur pour les listes de centaines de lignes.
Remontez le JSX statique
const STATIC_HEADER = <h1>Titre</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG : réduisez 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 un 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.
Supprimez les avertissements d'hydratation attendus de manière ciblée
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
À utiliser 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 de l'arbre et les effets montés mais les masque — moins coûteux que démonter/remonter pour les onglets et les accordéons.
Opérateur ternaire plutôt que && pour le rendu conditionnel
// INCORRECT — `0` est rendu comme 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'UI précédente comme isPending pendant que l'état suivant se calcule.
Indices de ressources React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com");