JSX n’est pas HTML : les véritables compromis derrière le markup des composants
Comprendre ce que l’on perd lorsque le markup devient JSX, des outils et du parsing aux formulaires, à l’accessibilité et à la portabilité, ainsi que les stratégies permettant de récupérer une grande partie de ce qui a été perdu.
Presque tous les tutoriels React rassurent les nouveaux venus en affirmant que JSX est « essentiellement du HTML à l’intérieur de JavaScript ». La ressemblance est réelle, mais ce rassurant mensonge cache une série de coûts qui se manifestent plus tard dans les pipelines de compilation, la taille des fichiers compilés, le temps de réponse et les audits d’accessibilité. Ci-dessous, vous verrez ce que JSX est réellement en dehors de son apparence extérieure, quels fonctionnalités changent lorsque le markup quitte le parseur du navigateur pour un environnement de exécution JavaScript, et quels modèles concrets permettent de récupérer la majeure partie de ce qui est perdu sans renoncer aux composants.
Un aspect familier sous un autre angle
JSX emprunte presque entièrement l’aspect extérieur d’HTML. Les éléments commencent par <button>, se terminent par </button> et s’empilent de la même manière que le markup depuis les années 1990. Cette familiarité a grandement facilité le passage aux applications single-page pour toute une génération de développeurs :
// It looks like HTML...
function UserCard({ name, role }) {
return (
<div className="card">
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
Cependant, au fond, il s’agit de deux technologies sans rapport. HTML est un langage de balisage déclaratif que les moteurs de navigateur analysent directement à l’aide de code natif fortement optimisé. JSX est une syntaxe que le compilateur réécrit en appels de fonctions JavaScript imbriqués : React.createElement dans la transformation classique, ou des outils comme _jsx() dans le runtime automatique moderne. Le <div> au sein d’un composant n’est pas du tout un balisage ; c’est une liste d’arguments.
Le passage à JSX vous offre de nombreux avantages : des arbres de composants gérés par les données, une synchronisation automatique entre l’état et le DOM, ainsi qu’une vérification de type pour vos templates. Cependant, toute abstraction a un prix. Remplacer un format natif du navigateur par une couche JavaScript signifie dépendre d’outils de build, consommer plus de mémoire en temps de exécution, et contourner plusieurs fonctionnalités de résilience fournies gratuitement par la plateforme. Aucun de ces inconvénients ne justifie d’éviter JSX, mais il est important de les connaître lors de la conception d’une application.
La taxe liée aux outils
Perte du flux de travail double-clic
La première chose qui disparaît, c’est la simplicité de la plateforme web, caractérisée par l’absence totale de dépendances. Une page HTML ordinaire ne nécessite rien d’autre qu’un éditeur et un navigateur. Vous pouvez créer index.html sur une machine hors ligne, faire un double-clic dessus, et le navigateur l’affiche immédiatement à partir d’une URL file://.
Aucun moteur JavaScript, que ce soit V8, JavaScriptCore ou SpiderMonkey, ne comprend <div className="box"> comme du code. Avant que quoi que ce soit n’apparaisse à l’écran, JSX nécessite une chaîne de compilation :
- un compilateur tel que Babel, SWC ou esbuild
- un bundler tel que Vite, Webpack, Rollup ou Turbopack
- npm, pnpm ou yarn pour installer les dépendances
- Node.js ou Bun pour exécuter ces outils
- un dossier
node_modulesqui pèse généralement des centaines de mégaoctets de préconfigurations, analyseurs, plugins et polyfills
(Il existe des versions de Babel en navigateur pour des expérimentations rapides, mais elles ne sont pas destinées à une diffusion en production.) La différence entre le chemin du fichier source et les pixels se présente comme suit :
HTML Workflow:
[index.html] ----------> Directly parsed by Browser Engine (Instant)
JSX Workflow:
[Component.jsx]
└─> AST Parsing
└─> Transpilation (_jsx() calls)
└─> Bundling & Minification
└─> Network Download
└─> JS Parse & Compile
└─> Runtime Virtual DOM
└─> DOM Mutation
La charge de maintenance
Le couplage du markup à un compilateur entraîne des coûts continus :
- Dégradation de la chaîne d’outils. Un projet à cui personne n’a touché depuis trois ans refuse souvent de se compiler, car les paquets sources, les versions requises de Node.js ou les API des outils de bundling ont évolué.
- Fragilité des cartes de source. Le débogage en environnement de production nécessite de relier le code minifié aux composants originaux. Lorsque les cartes de source manquent ou sont incorrectes, les traces d’erreur affichent des appels anonymes plutôt que votre propre code.
- Délai de retour des résultats de compilation. Les outils de bundling modernes écrits en Rust ou Go sont extrêmement rapides, mais dans des bases de code très volumineuses, la recompilation continue entraîne encore un retard qui n’existe pas lors de l’édition directe du markup.
Contraintes syntaxiques héritées de JavaScript
Puisque JSX est analysé comme du JavaScript, il hérite de ses mots réservés et de sa grammaire plus stricte, tout en perdant la flexibilité propre à HTML.
Les mots réservés deviennent des propriétés renommées
Un attribut HTML est une chaîne attachée à un nœud DOM. Une propriété JSX est une clé dans un objet transmis à une fonction. Comme class et for sont des mots réservés en JavaScript, JSX utilise d’autres noms à leur place. Le formulaire HTML standard :
<!-- Native, standards-compliant HTML -->
<label for="username">Username</label>
<div class="profile-card" tabindex="0"></div>
devient ce qui suit en JSX. Remarquez également que tabIndex reçoit une expression numérique plutôt qu’une chaîne :
// JSX equivalents forced by JavaScript engine constraints
<label htmlFor="username">Username</label>
<div className="profile-card" tabIndex={0}></div>
Sensibilité à la casse et objets de style
Les noms d’attributs HTML sont insensibles à la casse, tandis que les props de JSX le sont et suivent généralement la convention camelCase : onClick, strokeWidth, autoComplete, tabIndex. Une correction à une affirmation courante : les attributs aria-* et data-* constituent l’exception, conservant leurs noms contenant des traits d’union dans JSX ; ainsi aria-label s’écrit exactement comme en HTML.
Les styles en ligne entraînent des changements plus fondamentaux. En HTML, un style est une simple chaîne de caractères que le navigateur analyse :
<!-- HTML: Zero runtime allocation -->
<div style="background-color: red; margin-top: 10px;"></div>
En JSX, il s’agit d’un objet littéral JavaScript, et un tel objet écrit à l’intérieur du rendu est créé à nouveau chaque fois que le composant est affiché :
// JSX: Allocates a new JavaScript object on every single render pass
<div style={{ backgroundColor: 'red', marginTop: '10px' }} />
Pour la plupart des composants, cela est négligeable. Dans les composants qui se rérendent très fréquemment, tels que de grands tableaux de données ou des superpositions sur canvas gérées par des pointeurs, des milliers d’objets de style à durée de vie courte génèrent une pression sur le collecteur de déchets qui peut se manifester par des pauses. En extrayant les objets de style statiques du composant ou en utilisant des noms de classe, on peut éviter ce problème.
Règles strictes de fermeture
HTML5 tolère délibérément les éléments vides sans barres de fermeture : <input>, <img>, <br> et <hr> sont tous valides tels qu’écrits. JSX, quant à lui, suit les règles XML. Oublier la barre de fermeture automatique ou une balise de fermeture fait que le compilateur s’arrête avec une erreur de syntaxe, ce qui entraîne l’échec de la compilation plutôt qu’un fonctionnement dégradé.
Cost en temps de exécution : analyse native versus DOM virtuel
Lorsque le navigateur reçoit du HTML, il tokenise les octets au fur et à mesure qu’ils arrivent et construit progressivement des nœuds DOM, en utilisant du code natif affiné au fil des décennies et aidé par un analyseur de préchargement spéculatif qui découvre les ressources tôt. Lorsqu’une application s’affiche entièrement via JSX, ce chemin natif est largement contourné au profit de l’exécution du JavaScript :
Standard HTML Processing:
Network Bytes ──> Tokenizer ──> DOM Tree ──> Render Tree ──> Paint
JSX / Virtual DOM Processing:
Network Bytes ──> JS Parse/Compile ──> JS Execution ──> Component Tree
──> VDOM Allocation ──> VDOM Diffing (Reconciliation)
──> DOM Patching ──> Render Tree ──> Paint
Mémoire et collecte des déchets
Pour calculer les mises à jour, React conserve une description de l’interface utilisateur en mémoire JavaScript, communément appelée DOM virtuel. Le cycle fonctionne plus ou moins comme suit :
- La première affichage crée un arbre d’objets JavaScript décrivant chaque élément, ses propriétés et ses enfants.
- Lorsque l’état change, les composants concernés sont exécutés à nouveau et génèrent une nouvelle description de leur partie de l’arbre.
Notez que React redessine uniquement le sous-arbre situé en dessous du composant dont l’état a changé, et non toute l’application. Le principe reste cependant le même : le navigateur conserve déjà le DOM réel dans sa propre mémoire interne, tandis que la pile JavaScript contient également une représentation parallèle. Cette allocation supplémentaire augmente l’utilisation de la mémoire ainsi que la fréquence de collecte des déchets, ce qui est particulièrement visible sur les appareils mobiles peu puissants.
Le coût de l’hydratation
Le rendu du côté serveur dans des frameworks tels que Next.js ou Remix envoie un HTML réel, ce qui permet une affichage rapide dès le premier rendu. Cependant, avant que la page ne réagisse aux entrées de l’utilisateur, le navigateur doit télécharger le JavaScript des composants qui la composent, l’exécuter, reconstruire l’arbre interne de React et associer des gestionnaires d’événements au DOM existant. Cette étape d’hydratation occupe le thread principal, et sur les pages lourdes elle se traduit par un temps de blocage total élevé (TBT) ainsi que par une faible note d’interaction jusqu’au prochain rendu (INP).
Débit continu et tolérance aux pannes
L’HTML a été conçu autour de deux principes que les applications monopage axées sur JSX compromettent souvent : le débit continu incrémental et la tolérance aux erreurs.
Lorsque le débit continu disparaît
<head> ainsi que 50 KB d’une page de 200 KB ; le navigateur peut déjà demander les feuilles de style et les polices, ainsi que dessiner l’en-tête et la barre de navigation, tandis que le reste est encore en transit.
- le navigateur reçoit une structure presque vide contenant uniquement
<div id="root"></div> - il télécharge le paquet JavaScript
- il analyse et exécute ce paquet
- les composants s’exécutent, construisent le DOM et affichent enfin le contenu
Avec une connexion 3G lente ou un téléphone peu puissant, l’utilisateur voit une page blanche pendant toute cette séquence. C’est tout ce avec quoi le navigateur doit commencer à travailler :
<!-- What the browser sees initially in a standard JSX SPA -->
<!DOCTYPE html>
<html>
<head>
<title>App</title>
</head>
<body>
<div id="root"></div>
<script src="/static/bundle.8f9b2c.js"></script>
</body>
</html>
Lorsque les erreurs ne sont plus tolérées
Le parseur HTML est réputé pour sa tolérance. Donnez-lui du markup défectueux comme ceci :
<div>
<p>Unclosed paragraph
<div>Nested incorrectly</b>
</div>
et il ne plante pas. L’algorithme de parsing ferme et ré-encastre les éléments selon des règles de récupération bien définies, affichant néanmoins le contenu. React est beaucoup moins indulgent en temps de exécution. Si l’affichage échoue, par exemple parce qu’une expression comme {user.profile.name} tente d’accéder à une propriété de undefined, React démonte tout l’arbre si aucun Error Boundary ne capture l’erreur, laissant ainsi une page vide. React ne fournit pas de composant <ErrorBoundary> prêt à l’emploi ; vous devez en écrire un en tant que composant de classe (ou utiliser une petite bibliothèque) et le placer délibérément autour des zones à risque.
Formulaires et événements
Les formulaires HTML gèrent nativement les entrées, la validation et l’envoi depuis les débuts du web. Les patterns typiques de JSX remplacent souvent ces mécanismes primitifs par des réimplémentations en JavaScript.
Entrées contrôlées versus entrées natives
<input> conserve son propre état. La saisie met immédiatement à jour le buffer interne du navigateur, sans aucune intervention de script. Le pattern idiomatique React, en revanche, rend l’entrée contrôlée, de sorte que l’état React devient la source de vérité :
// Every keystroke triggers a state change, a re-render, and a VDOM diff
function SearchInput() {
const [value, setValue] = useState("");
return (
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
Désormais, chaque frappe passe par le système d’événements, modifie l’état, exécute à nouveau la fonction du composant, effectue une mise à jour, puis renvoie la valeur dans le DOM. Cela fonctionne bien pour un petit composant. Lorsque le thread principal est occupé par le traitement de données ou des animations lourdes, ou lorsque l’entrée se trouve à l’intérieur d’un grand arbre de composants qui se rérendre automatiquement avec elle, les utilisateurs peuvent observer des caractères apparaître de manière perceptible après qu’ils les aient tapés.
Événements synthétiques
React enveloppe les événements natifs du navigateur dans son propre système d’événements synthétiques, à l’origine afin de combler les incohérences entre anciens navigateurs. Cette abstraction entraîne cependant certains problèmes cachés :
- la propagation à travers l’arbre de React peut différer de celle via les écouteurs natifs du DOM, ce qui complique le code qui utilise les deux
Accessibilité et dérive sémantique
JSX peut générer du HTML parfaitement accessible et sémantique. Cependant, les patterns qu’il encourage ont tendance à éroder la sémantique avec le temps.
Soupe de divs issue des enveloppes de composants
Un composant doit retourner un seul nœud racine. Les fragments (<Fragment> ou <>) résolvent ce problème sans ajouter de DOM supplémentaire, mais de nombreux projets continuent d’envelopper les enfants dans des conteneurs <div> par habitude ou pour des raisons de mise en page. La structure que vous vouliez créer est la suivante :
<!-- What you intended to build -->
<main>
<article>
<h1>Article Title</h1>
<p>Content goes here...</p>
</article>
</main>
Alors que les composants d’enveloppe en plusieurs couches affichent souvent quelque chose de plus proche de ceci :
<!-- What JSX component wrapping often generates in the actual DOM -->
<div class="AppWrapper">
<div class="LayoutContainer">
<main>
<div class="ArticleWrapper">
<article>
<div class="HeadingGroup">
<h1>Article Title</h1>
</div>
<div class="ParagraphContainer">
<p>Content goes here...</p>
</div>
</article>
</div>
</main>
</div>
</div>
Ces couches supplémentaires alourdissent le DOM, compliquent la mise en page CSS et créent du bruit entre les éléments importants. Les <div> génériques sans rôles sont généralement ignorés par les technologies d’assistance, mais des chaînes d’enveloppe profondes rendent encore plus difficile la compréhension du code et augmentent les risques d’erreurs, par exemple lorsque l’enveloppe perturbe accidentellement une structure de liste ou de titre.
Comportement clavier offert gratuitement, jusqu’à ce qu’il ne le soit plus
Les éléments interactifs natifs tels que <button>, <a>, <select> et <details> disposent de comportements que l’on prend souvent pour acquis :
- Ils sont par défaut accessibles en suivant l’ordre Tab
- Les touches Entrée et Espace les activent automatiquement
Puisque JSX rend très simple l’attachement d’un gestionnaire de clic à n’importe quel élément, comme dans <div onClick={handleClick}>, les équipes créent régulièrement des contrôles personnalisés à partir d’éléments non sémantiques et oublient les gestionnaires de clavier, tabIndex ainsi que les rôles ARIA que ces éléments natifs fournissent automatiquement. Notre aperçu des pièges cachés des composants React aborde davantage de ces difficultés.
Normes, interopérabilité et verrouillage
HTML est un standard ouvert géré par le WHATWG, avec la participation historique du W3C. Les pages écrites en 1997 s’affichent encore dans les navigateurs actuels. En revanche, JSX n’est pas un standard web. Il dispose d’une spécification informelle et est pris en charge par plusieurs bibliothèques, dont React, Preact et Solid, mais chaque utilisation dépend d’un compilateur ainsi que du runtime ciblé par ce dernier. Il s’agit d’une forme moins stricte de verrouillage que pour un format propriétaire, mais il y a néanmoins verrouillage.
Éléments personnalisés et le modèle de composants propre à la plateforme
Les navigateurs disposent déjà d’un modèle de composants : les Éléments Personnalisés associés au Shadow DOM. Le HTML classique les utilise directement :
<user-avatar src="avatar.jpg" size="large"></user-avatar>
Historiquement, React a mal géré les éléments personnalisés pour deux raisons :
- il transmettait tous les props aux balises en minuscules inconnues sous forme d’attributs de chaîne, ce qui empêchait les objets et les tableaux d’être transmis en tant que propriétés des éléments
new CustomEvent('user-select'), ne correspondaient pas à une propriété comme onUserSelect, ce qui obligeait les composants conteneurs ou des écouteurs manuels via des refsReact 19 a résolu une grande partie de ce problème en permettant d’attribuer des propriétés aux éléments personnalisés lorsque ces derniers les définissent, ainsi qu’en prenant en charge des gestionnaires d’événements personnalisés ; vérifiez donc quelle version de React vous utilisez avant de supposer que ces limitations s’appliquent encore.
Portabilité du markup
Un système de conception écrit en HTML et CSS standard peut être utilisé n’importe où : WordPress, Django, Ruby on Rails, des templates Go, Vue, Angular, Svelte ou des pages statiques classiques. Un système de conception écrit sous forme de composants JSX est lié à l’écosystème JavaScript. L’utiliser avec un backend non JavaScript nécessite un service de rendu Node.js ou une deuxième implémentation de chaque composant.
HTML et JSX côte à côte
Résumé des compromis :
- Exécution : L’HTML est analysé nativement par le navigateur ; JSX se compile en appels de fonction JavaScript qui s’exécutent en temps de exécution.
- Outils : L’HTML nécessite un éditeur et un navigateur ; JSX requiert un compilateur, un bundler, un gestionnaire de paquets et un environnement d’exécution.
- Syntaxe : L’HTML est insensible à la casse et tolérant ; JSX est sensible à la casse, utilise des propriétés renommées et exige une fermeture au style XML.
- Rendu : L’HTML génère des flux et les affiche progressivement ; le JSX rendu côté client attend que le bundle soit téléchargé et exécuté.
- Erreurs : L’HTML se remet d’une balise mal formatée ; une erreur de rendu non capturée démonte l’arbre React.
- Formulaires : Les champs natifs conservent leur propre état ; les champs contrôlés se rérendent à chaque frappe.
Récupérer ce que vous avez perdu
Tout cela ne vise pas à abandonner le développement basé sur des composants. Il s’agit plutôt de décider avec précision où l’abstraction en vaut la peine. L’écosystème évolue précisément dans cette direction, avec des patterns qui restituent la vitesse et la résilience de l’HTML natif tout en conservant un modèle d’écriture déclaratif.
Choisir le rendu server-first
- React Server Components se rendent sur le serveur et n’envoient aucun JavaScript de composant au client pour les parties qui ne sont pas interactives.
- Astro utilise une architecture par îlots : les pages sont des HTML statiques par défaut, et seuls des widgets interactifs isolés sont hydratés.
- Qwik remplace l’hydratation par la capacité de reprise, en serialisant l’état de l’application dans l’HTML afin que le code ne s’exécute que lorsque l’utilisateur interagit réellement.
Laissez le navigateur gérer l’état du formulaire
Au lieu de refléter chaque frappe dans l’état, laissez le <form> natif conserver les valeurs et les lire une seule fois lors de l’envoi à l’aide de FormData:
// Clean, native, performant HTML-first form submission
function LoginForm() {
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email");
// Send payload...
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" required />
<button type="submit">Sign In</button>
</form>
);
}
L’entrée s’actualise à la vitesse native, l’attribut required offre une validation intégrée, et le composant s’affiche une seule fois plutôt qu’à chaque frappe. Les versions récentes de React s’appuient sur la même idée avec les actions de formulaire, qui acceptent directement FormData.
Gardez la sémantique stricte
Considérez JSX comme un moyen de produire du HTML sémantique, et non comme une autorisation à empiler des conteneurs :
- remplacez les balises d’enveloppe
<div>par des fragments (<></>) chaque fois que l’enveloppe n’a pas de fonction esthétique - utilisez des éléments interactifs natifs tels que
<button>,<dialog>,<details>et<summary>au lieu de widgets personnalisés - intégrez
eslint-plugin-jsx-a11ydans le processus d’intégration continue afin que l’absence de balises, de rôles ou de gestionnaires de clavier empêche la compilation
En résumé
JSX a transformé le développement front-end en montrant que les interfaces se décrivent le mieux comme des fonctions prévisibles des données, et il a résolu de vrais problèmes liés au maintien en synchronisation d’interfaces utilisateur grandes et dynamiques avec l’état. Il s’agit toujours d’une abstraction JavaScript, et non d’une version plus récente d’HTML. Le choisir signifie sacrifier le streaming natif, la création sans compilation, la tolérance aux erreurs et la stabilité à long terme des normes au profit de la composition et d’une ergonomie réactive. C’est souvent un bon échange. La compétence technique réside dans le fait de savoir exactement ce que l’on sacrifie et dans le recours au rendu serveur, aux formulaires natifs et aux éléments sémantiques chaque fois que l’on peut récupérer ces fonctionnalités gratuitement.
Lectures complémentaires
- REST vs GraphQL : Les vrais compromis à l’origine de chaque architecture — Explique les problèmes spécifiques que résolvent respectivement REST et GraphQL, leurs mécanismes internes, ainsi que les compromis cachés à prendre en compte avant de choisir l’un d’eux pour votre API.
- Dix erreurs cachées dans les composants React qui ralentissent les applications modernes — Découvrez dix erreurs courantes dans les composants React, allant de lacunes dans l’HTML sémantique à l’absence de mémoisation, ainsi que les correctifs nécessaires pour maintenir des applications rapides, accessibles et sans erreurs en 2026.