Aller au contenu principal

Pourquoi votre application React est lente en 2026 ?

Publié le

Dimitri Dumont
Dimitri Dumont

Développeur React & Next.js freelance

Une application React qui rame, c'est un problème courant et rarement dû à une seule cause. Le réflexe habituel est d'ajouter des useMemo partout en espérant que ça aille mieux. C'est l'erreur la plus fréquente : on optimise au hasard, sans savoir où est le vrai goulot d'étranglement.

La bonne approche est l'inverse : mesurer d'abord, localiser la cause racine, corriger, puis re-mesurer pour prouver le gain. Cet article décrit cette méthode et les causes les plus fréquentes d'une application React lente en 2026.

Dans cet article :

  • Comment mesurer les performances avant de toucher au code
  • Les causes fréquentes d'une application React lente, par symptôme
  • Ce que le React Compiler règle, et ce qu'il ne règle pas
  • Pourquoi le code généré par IA crée de nouvelles lenteurs
  • Une méthode de diagnostic en quatre étapes

Mesurer avant d'optimiser

Optimiser sans mesurer revient à corriger un bug sans le reproduire. La première étape est toujours de quantifier le problème avec les Core Web Vitals.

  • LCP (Largest Contentful Paint) : temps d'affichage du plus gros élément visible. Mesure la vitesse de chargement. Cible : moins de 2,5 secondes.
  • INP (Interaction to Next Paint) : délai entre une interaction (clic, saisie, ouverture d'un menu) et la réponse visible à l'écran. Mesure la réactivité. Cible : moins de 200 millisecondes. L'INP a remplacé le FID comme Core Web Vital, et c'est aujourd'hui la métrique la plus souvent en échec.
  • CLS (Cumulative Layout Shift) : stabilité visuelle, c'est-à-dire les éléments qui sautent pendant le chargement. Cible : moins de 0,1.

Les outils de diagnostic

Chaque outil répond à une question différente :

  • Lighthouse : un audit synthétique avec des recommandations rattachées à chaque Core Web Vital. Idéal pour un premier état des lieux. Pour l'automatiser sur chaque déploiement, voyez intégrer Lighthouse CI dans un projet Next.js.
  • React DevTools Profiler : enregistre les rendus et montre quels composants se re-rendent, à quelle fréquence et pourquoi. C'est l'outil clé pour traquer les re-renders.
  • Onglet Performance de Chrome DevTools : profile une interaction précise pour voir ce qui bloque le thread principal (l'INP se diagnostique ici).
  • @next/bundle-analyzer : visualise le poids du bundle JavaScript sous forme de carte. On y repère les dépendances surdimensionnées.

Une distinction qui compte : Lighthouse et l'onglet Performance tournent en laboratoire, sur une seule visite. L'INP, lui, dépend des interactions réelles des utilisateurs et se mesure sur le terrain, via les données CrUX (PageSpeed Insights) ou la librairie web-vitals. En laboratoire, le Total Blocking Time en donne un proxy, mais il ne remplace pas la mesure terrain.

Les causes fréquentes d'une application React lente

La cause racine se résume souvent à une chose : trop de travail sur le thread principal, au mauvais moment. Voici les symptômes les plus courants et leur traitement.

SymptômeCause probableOutilCorrectif
Chargement initial lent (LCP)Bundle JavaScript trop lourdbundle-analyzerCode splitting, dynamic import
Interface qui « lague » (INP)Re-renders en cascadeReact ProfilerPlacement de l'état, mémoïsation
Trop de JavaScript envoyé"use client" trop hautbundle-analyzerServer Components
Page lente à afficher des donnéesRequêtes en cascadeOnglet NetworkRequêtes parallèles, cache
Écran vide, puis spinner, puis donnéesFetch au montage côté clientOnglet NetworkFetch serveur, prefetch, squelette
Longue liste ou tableau qui saccadeTous les éléments rendus d'un coupReact ProfilerVirtualisation
Éléments qui sautent (CLS)Images sans dimensionsLighthousenext/image, tailles réservées

Un bundle JavaScript trop lourd

Plus vous envoyez de JavaScript au navigateur, plus il met de temps à devenir interactif. Une dépendance lourde chargée d'entrée pénalise le LCP. La solution est de sortir un composant coûteux du bundle initial pour qu'il se charge après le contenu critique, sans bloquer le premier rendu.

app/report/ReportView.tsx
import dynamic from "next/dynamic"
 
// Le graphique lourd part dans un chunk séparé, hors du bundle initial
const HeavyChart = dynamic(() => import("./HeavyChart"))
 
export const ReportView = () => (
	<section>
		<Summary />
		<HeavyChart />
	</section>
)

Des re-renders en cascade

Par défaut, quand un état change dans un composant, ce composant et ses descendants se re-rendent. Un état à haute fréquence (une saisie, une position de défilement) placé trop haut élargit inutilement cette zone et dégrade l'INP. Le React Compiler limite les dégâts en évitant de re-rendre les composants dont les props n'ont pas changé, mais le plus sain reste de rapprocher l'état de là où il sert.

components/Dashboard.tsx (avant)
"use client"
 
// L'état de recherche fait re-rendre tout le tableau de bord à chaque frappe
export const Dashboard = () => {
	const [query, setQuery] = useState("")
	return (
		<>
			<SearchInput value={query} onChange={setQuery} />
			<ExpensiveStats />
			<ResultsTable query={query} />
		</>
	)
}
components/Dashboard.tsx (après)
// L'état vit au plus près de là où il est utilisé
export const Dashboard = () => (
	<>
		<ExpensiveStats />
		<SearchableResults />
	</>
)

Ce type de re-render fait partie des anti-patterns React les plus courants.

Un "use client" posé trop haut (Next.js)

Dans Next.js, le plus gros levier de performance est d'arrêter de mettre "use client" par réflexe en haut de l'arbre. Un Server Component n'envoie aucun JavaScript au navigateur. Le bon pattern est de garder le parent côté serveur et d'isoler l'interactivité dans un petit îlot client.

app/page.tsx (Server Component)
import { LikeButton } from "./LikeButton"
 
// Pas de "use client" ici : ce composant ne part pas dans le bundle
export default async function Page() {
	const article = await getArticle()
	return (
		<article>
			<h1>{article.title}</h1>
			<p>{article.body}</p>
			<LikeButton id={article.id} />
		</article>
	)
}
app/LikeButton.tsx (Client Component)
"use client"
 
import { useState } from "react"
 
export const LikeButton = ({ id }: { id: string }) => {
	const [liked, setLiked] = useState(false)
	return (
		<button onClick={() => setLiked(!liked)}>{liked ? "♥" : "♡"}</button>
	)
}

Des requêtes de données en cascade

Une page qui enchaîne des await séquentiels attend chaque requête l'une après l'autre. Lancer les requêtes indépendantes en parallèle réduit directement le temps d'affichage.

app/dashboard/page.tsx
// Avant : requêtes en cascade, additionnées
const user = await getUser()
const orders = await getOrders()
 
// Après : requêtes parallèles, temps du plus lent
const [user, orders] = await Promise.all([getUser(), getOrders()])

Le détail de cette technique est dans utiliser Promise.all pour optimiser les performances, et la mise en cache des données serveur dans le guide de React.cache.

Des données affichées en deux temps

Un schéma très courant : un composant client se monte, affiche un écran vide, déclenche sa requête dans un useEffect, montre un spinner, puis enfin les données. L'utilisateur traverse trois états, souvent avec un saut de mise en page. Le réseau n'est pas forcément lent : la requête démarre trop tard (au montage) et rien de stable ne s'affiche pendant l'attente.

Deux leviers, dans cet ordre :

  • Démarrer la requête plus tôt. Récupérée côté serveur (Server Component ou loader de route), la donnée est déjà là au premier rendu, sans aller-retour client. Pour une vue ouverte à la demande (un dialog, un panneau latéral), on peut précharger au survol ou au focus du déclencheur, avant même le clic.
  • Afficher un état de chargement stable. Si la donnée doit venir du client, un squelette aux dimensions finales vaut mieux qu'un écran vide : pas de saut de mise en page, et l'attente paraît plus courte.
components/UserPanel.tsx (avant)
"use client"
 
// Écran vide au montage, puis spinner, puis données
export const UserPanel = ({ id }: { id: string }) => {
	const [user, setUser] = useState<User | null>(null)
 
	useEffect(() => {
		getUser(id).then(setUser)
	}, [id])
 
	if (!user) return <Spinner />
	return <UserCard user={user} />
}
app/users/[id]/page.tsx (après)
// La donnée est récupérée côté serveur : pas d'écran vide, pas de spinner au montage
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
	const { id } = await params
	const user = await getUser(id)
	return <UserCard user={user} />
}

Quand l'affichage dépend vraiment d'une action utilisateur, gardez le chargement côté client mais préchargez au survol et réservez la place avec un squelette plutôt qu'un écran vide :

components/Report.tsx
<Suspense fallback={<ReportSkeleton />}>
	<Report id={id} />
</Suspense>

Des listes longues rendues en entier

Afficher des milliers de lignes d'un coup gonfle le DOM et fait travailler le thread principal à chaque rendu et à chaque défilement, ce qui dégrade l'INP au scroll et au filtrage. La virtualisation ne monte que les éléments visibles (plus une petite marge), quelle que soit la taille de la liste.

components/BigList.tsx
"use client"
 
import { useRef } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
 
// Seules les lignes visibles sont montées, même si `items` en contient 10 000
export const BigList = ({ items }: { items: string[] }) => {
	const parentRef = useRef<HTMLDivElement>(null)
	const rows = useVirtualizer({
		count: items.length,
		getScrollElement: () => parentRef.current,
		estimateSize: () => 40,
	})
 
	return (
		<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
			<div style={{ height: rows.getTotalSize(), position: "relative" }}>
				{rows.getVirtualItems().map((row) => (
					<div
						key={row.key}
						style={{ position: "absolute", top: 0, transform: `translateY(${row.start}px)` }}
					>
						{items[row.index]}
					</div>
				))}
			</div>
		</div>
	)
}

React Compiler : ce qu'il règle, ce qu'il ne règle pas

Le React Compiler automatise la mémoïsation. Concrètement, il rend inutile la plupart des useMemo et useCallback écrits à la main pour éviter les rendus inutiles. C'est une avancée réelle, mais ce n'est pas une solution miracle.

Ce qu'il faut savoir :

  • Il exige que le code respecte les Rules of React. Un composant impur ou du code legacy peut ne tirer aucun bénéfice.
  • Il ne corrige pas un mauvais placement de l'état : si un état à haute fréquence est remonté trop haut, le parent continue de se re-rendre.
  • La mémoïsation automatique rend le débogage moins lisible, car les optimisations ne sont plus visibles dans le code.

Autrement dit, le React Compiler réduit le coût des re-renders, mais l'architecture des composants et le placement de l'état restent votre responsabilité.

L'IA, nouvelle source de lenteur en 2026

En 2026, une part croissante du code React est générée par des outils d'IA. Le problème : sans relecture, ce code reproduit les anti-patterns classiques, qui sont aussi des problèmes de performance.

Les cas les plus fréquents :

  • Des composants entièrement en "use client" alors qu'ils n'ont aucune interactivité.
  • Des useEffect superflus pour des données qui devraient venir du serveur.
  • Un état mal placé qui déclenche des re-renders inutiles.

L'IA accélère l'écriture du code, mais elle ne juge pas son impact sur les performances. C'est le rôle d'un développeur React senior qui relit ce qui est généré. J'ai détaillé les patterns que je refuse en revue de code généré par IA.

Une méthode de diagnostic en quatre étapes

Plutôt que d'optimiser au hasard, suivez une démarche reproductible.

  1. Mesurer : relevez les Core Web Vitals (Lighthouse pour le chargement, données CrUX pour l'INP de terrain) et profilez les rendus (React Profiler) ou l'interaction lente (Chrome DevTools Performance). Notez les chiffres de départ.
  2. Localiser : identifiez le vrai goulot. Un LCP élevé pointe vers le chargement (bundle, données, images). Un INP élevé pointe vers une interaction qui bloque le thread principal.
  3. Corriger la cause racine : traitez le bottleneck identifié, pas un symptôme voisin. Une seule correction ciblée vaut mieux que dix micro-optimisations.
  4. Re-mesurer : comparez aux chiffres de départ. Sans cette étape, vous ne savez pas si le changement a aidé, ni de combien.

Cette boucle évite la dépense d'énergie la plus courante : optimiser ce qui n'était pas le problème.

Les erreurs à éviter

  • Optimiser sans mesurer. Si vous ne pouvez pas chiffrer le gain, vous ne savez pas si vous avez aidé.
  • Mettre useMemo et useCallback partout « au cas où ». Avec le React Compiler, c'est souvent inutile, et ça alourdit le code.
  • Faire des micro-optimisations pendant que le vrai goulot est ailleurs. Profilez avant de toucher quoi que ce soit.
  • Confondre LCP et INP. Une application peut charger vite mais répondre lentement aux interactions, ou l'inverse. Les correctifs sont différents.
  • Laisser un écran vide pendant le chargement. Un fetch déclenché au montage côté client, sans squelette ni préchargement, allonge la lenteur perçue et provoque des sauts de mise en page.
  • Mettre "use client" en haut de l'arbre par réflexe. Chaque composant client est du JavaScript envoyé au navigateur.

FAQ

Pourquoi mon application React est-elle lente ?

Le plus souvent à cause d'un bundle JavaScript trop lourd, de re-renders en cascade, ou d'un "use client" posé trop haut dans l'arbre. La seule façon de savoir laquelle s'applique est de mesurer avec Lighthouse et le React Profiler. Optimiser sans cette étape revient à deviner.

Comment mesurer les performances d'une application React ?

Lighthouse pour le LCP et le CLS en laboratoire, les données de terrain (CrUX via PageSpeed Insights, ou la librairie web-vitals) pour l'INP, le React DevTools Profiler pour les rendus, et l'onglet Performance de Chrome DevTools pour disséquer une interaction lente. Lighthouse donne l'état des lieux du chargement, mais l'INP réel ne se voit que sur le terrain ; le Profiler localise les composants qui se re-rendent trop, et l'onglet Performance révèle ce qui bloque le thread principal.

Le React Compiler suffit-il à rendre mon application rapide ?

Non. Le React Compiler automatise la mémoïsation et supprime le besoin de useMemo manuels, mais il ne corrige pas un mauvais placement de l'état, ni un bundle trop lourd, ni des requêtes en cascade. C'est une aide, pas un substitut à une bonne architecture.

useMemo et useCallback améliorent-ils vraiment les performances ?

Seulement dans des cas précis, et de moins en moins depuis le React Compiler. Utilisés partout « au cas où », ils ajoutent de la complexité sans gain mesurable. Avec le compilateur activé, la mémoïsation manuelle devient redondante dans la majorité des composants.

Faut-il réécrire l'application ou peut-on l'optimiser ?

Dans la grande majorité des cas, une optimisation ciblée suffit. Une réécriture complète se justifie rarement par les seules performances. Un diagnostic identifie les quelques corrections à fort impact, ce qui évite le coût et le risque d'une réécriture.

Conclusion

Une application React lente n'a presque jamais une cause unique, et la résoudre ne consiste pas à ajouter des optimisations au hasard. La démarche qui fonctionne est méthodique : mesurer, localiser le vrai goulot, corriger la cause racine, re-mesurer.

Points clés

  1. Mesurez avant d'optimiser, sinon vous ne faites que deviner.
  2. Les trois causes dominantes sont le bundle, les re-renders et le "use client" trop haut.
  3. L'INP (réactivité) est la métrique la plus échouée en 2026, distincte du LCP (chargement).
  4. Le React Compiler réduit le coût des re-renders, mais ne remplace pas une bonne architecture.
  5. Le code généré par IA demande une relecture : il reproduit les anti-patterns qui dégradent les performances.
Dimitri Dumont

À propos de l'auteur

Je suis Dimitri Dumont, développeur freelance spécialisé React & Next.js depuis plus de 8 ans. J'ai accompagné 22 startups et réalisé 44 missions avec une note de 5/5. Je partage ici les pratiques que j'utilise en mission. En savoir plus →

Cet article vous a été utile ?

Je peux vous accompagner sur votre projet React & Next.js.

Échanger sur votre projet →

Articles similaires