JSX no es HTML: Las verdaderas concesiones detrás del marcado de componentes
Comprender qué se pierde cuando el marcado pasa a ser JSX, desde las herramientas y el análisis hasta los formularios, la accesibilidad y la portabilidad, así como los patrones que permiten recuperar gran parte de ello.
Casi todos los tutoriales de React aseguran a los principiantes que JSX es “básicamente HTML dentro de JavaScript”. La similitud existe, pero esa afirmación oculta una serie de desventajas que se manifiestan más adelante en los procesos de compilación, el tamaño de los paquetes, la latencia de entrada y las auditorías de accesibilidad. A continuación, verá cuál es realmente JSX más allá de su apariencia, qué capacidades cambian cuando el marcado pasa del analizador del navegador a un entorno de ejecución de JavaScript, y qué patrones concretos permiten recuperar la mayor parte de lo perdido sin renunciar a los componentes.
Una mirada familiar desde una perspectiva diferente
JSX toma prestado casi por completo el aspecto superficial de HTML. Los elementos comienzan con <button>, terminan con </button> y se anidan de la misma manera que el marcado lo ha hecho desde la década de 1990. Esa familiaridad facilitó enormemente el paso a las aplicaciones de página única para toda una generación de desarrolladores:
// It looks like HTML...
function UserCard({ name, role }) {
return (
<div className="card">
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
Aun así, en realidad se trata de tecnologías no relacionadas. HTML es un lenguaje de marcado declarativo que los motores de navegador interpretan directamente con código nativo altamente optimizado. JSX es una sintaxis que un compilador reescribe en llamadas a funciones anidadas de JavaScript: React.createElement en la transformación clásica, o ayudantes como _jsx() en el entorno de ejecución automático moderno. El <div> dentro de un componente no es marcado en absoluto; es una lista de argumentos.
Usar JSX te brinda muchas ventajas: árboles de componentes controlados por datos, sincronización automática entre el estado y el DOM, y verificación de tipos en tus plantillas. Sin embargo, toda abstracción tiene su precio. Sustituir un formato nativo del navegador por una capa de JavaScript implica depender de herramientas de compilación, consumir más memoria en tiempo de ejecución y omitir varias funciones de resiliencia que la plataforma ofrece gratuitamente. Ninguno de estos costos es motivo para evitar JSX, pero cada uno merece ser tenido en cuenta al diseñar una aplicación.
El costo de las herramientas
Pérdida del flujo de trabajo de doble clic
Lo primero que desaparece es la simplicidad de cero dependencias de la plataforma web. Una página HTML sencilla no necesita nada más que un editor y un navegador. Puedes crear index.html en una máquina sin conexión, hacer doble clic en él y el navegador lo renderizará inmediatamente desde una URL file://.
Ningún motor de JavaScript, ya sea V8, JavaScriptCore o SpiderMonkey, entiende <div className="box"> como código. Antes de que cualquier cosa llegue a la pantalla, JSX necesita una cadena de compilación:
- un compilador como Babel, SWC o esbuild
- un bundler como Vite, Webpack, Rollup o Turbopack
- npm, pnpm o yarn para instalar dependencias
- Node.js o Bun para ejecutar esas herramientas
- una carpeta
node_modulesque suele pesar cientos de megabytes con presets, analizadores, plugins y polyfills
(Existen versiones de Babel para el navegador para experimentos rápidos, pero no son algo que se incluya en la versión final.) La diferencia en el camino desde el archivo de origen hasta los píxeles es la siguiente:
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 carga de mantenimiento
Vincular el marcado a un compilador genera costos continuos:
- Rotura de la cadena de herramientas. Un proyecto al que nadie ha tocado en tres años a menudo se niega a compilarse, porque los paquetes de origen, las versiones requeridas de Node.js o las API del bundler han evolucionado.
- Fragilidad de los mapas de fuente. Depurar en entornos de producción implica mapear la salida minificada de vuelta a los componentes originales. Cuando faltan o están incorrectos los mapas de fuente, las trazas de stack muestran llamadas anónimas en tiempo de ejecución en lugar de tu código.
- Retraso en las respuestas de compilación. Los bundlers modernos escritos en Rust o Go son extremadamente rápidos, pero en bases de código muy grandes la recompilación continua sigue generando un retraso que simplemente no existe al editar marcado en bruto.
Restricciones sintácticas heredadas de JavaScript
Dado que JSX se analiza como JavaScript, hereda las palabras reservadas y la gramática más estricta de este último, perdiendo así la flexibilidad propia de HTML.
Las palabras reservadas pasan a ser propiedades con nombres diferentes
Un atributo HTML es una cadena adjunta a un nodo DOM. Una propiedad JSX es una clave en un objeto pasado a una función. Dado que class y for son palabras reservadas en JavaScript, JSX utiliza otros nombres en su lugar. El formulario HTML estándar:
<!-- Native, standards-compliant HTML -->
<label for="username">Username</label>
<div class="profile-card" tabindex="0"></div>
se convierte en lo siguiente en JSX. También observe que tabIndex recibe una expresión numérica en lugar de una cadena:
// JSX equivalents forced by JavaScript engine constraints
<label htmlFor="username">Username</label>
<div className="profile-card" tabIndex={0}></div>
Sensibilidad a mayúsculas y minúsculas y objetos de estilo
Los nombres de los atributos HTML son insensibles a mayúsculas y minúsculas, mientras que los props de JSX son sensibles a mayúsculas y minúsculas y suelen seguir el formato camelCased: onClick, strokeWidth, autoComplete, tabIndex. Una corrección a una afirmación común: los atributos aria-* y data-* son la excepción y mantienen sus nombres con guiones en JSX, por lo que aria-label se escribe exactamente como en HTML.
Los estilos incrustados cambian de manera más fundamental. En HTML, un estilo es una cadena simple que el navegador analiza:
<!-- HTML: Zero runtime allocation -->
<div style="background-color: red; margin-top: 10px;"></div>
En JSX, es un objeto literal de JavaScript, y un literal escrito dentro del resultado de renderizado se crea de nuevo cada vez que se renderiza el componente:
// JSX: Allocates a new JavaScript object on every single render pass
<div style={{ backgroundColor: 'red', marginTop: '10px' }} />
Para la mayoría de los componentes esto es insignificante. En aquellos que se vuelven a renderizar con mucha frecuencia, como las grandes tablas de datos o las superposiciones en lienzo controladas por punteros, miles de objetos de estilo de corta duración generan presión en la recolección de basura que puede manifestarse como pausas. Evitar esto se logra sacando los objetos de estilo estáticos fuera del componente o utilizando nombres de clase.
Reglas estrictas de cierre
HTML5 tolera deliberadamente elementos vacíos sin barras de cierre: <input>, <img>, <br> y <hr> son válidos tal como están escritos. JSX, en cambio, sigue las reglas de XML. Si se olvida la barra de cierre automática o una etiqueta de cierre, el compilador detiene el proceso con un error de sintaxis, por lo que la construcción falla en lugar de funcionar de forma deficiente.
Costo en tiempo de ejecución: análisis nativo frente a un DOM virtual
Cuando el navegador recibe HTML, tokeniza los bytes a medida que llegan y construye los nodos DOM de forma incremental, utilizando código nativo perfeccionado a lo largo de décadas y con la ayuda de un escáner de carga previa especulativa que descubre los recursos con antelación. Cuando una aplicación se renderiza completamente mediante JSX, ese camino nativo se omite en gran medida en favor de la ejecución de 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
Memoria y recolección de basura
Para calcular las actualizaciones, React mantiene una descripción de la interfaz de usuario en la memoria de JavaScript, comúnmente llamada DOM virtual. El ciclo funciona más o menos de esta manera:
- La primera renderización crea un árbol de objetos JavaScript que describe cada elemento, sus propiedades y sus hijos.
- Cuando cambia el estado, los componentes afectados se ejecutan nuevamente y generan una nueva descripción de su parte del árbol.
Tenga en cuenta que React vuelve a renderizar el subárbol debajo del componente cuyo estado cambió en lugar de toda la aplicación, pero el principio sigue siendo el mismo: el navegador ya almacena el DOM real en su propia memoria interna, y la pila de JavaScript también contiene una representación paralela. Esa asignación adicional aumenta el uso de memoria y la frecuencia de recolección de basura, lo cual se nota más en dispositivos móviles de gama baja.
El costo de la hidratación
El renderizado del lado del servidor en frameworks como Next.js o Remix envía HTML real, lo que permite una carga rápida de la página. Sin embargo, antes de que la página responda a las entradas del usuario, el navegador debe descargar el JavaScript de los componentes que contiene, ejecutarlo, reconstruir el árbol interno de React y asignar manejadores de eventos al DOM existente. Este paso de hidratación ocupa el hilo principal, y en páginas complejas se traduce en un alto tiempo total de bloqueo (TBT) y una baja puntuación de interacción hasta la primera pintura (INP).
Transmisión por streaming y tolerancia a fallos
HTML fue diseñado sobre dos principios que las aplicaciones de página única centradas en JSX suelen socavar: la transmisión incremental y la tolerancia a errores.
Cuando desaparece el streaming
Un navegador comienza a renderizar un documento antes de que se haya descargado por completo. Supongamos que el servidor ha entregado hasta ahora el <head> más 50 KB de una página de 200 KB; el navegador ya puede solicitar hojas de estilo y fuentes, así como dibujar el encabezado y la navegación, mientras el resto sigue en tránsito.
Una aplicación JSX procesada exclusivamente en el cliente funciona de manera diferente:
- el navegador recibe una estructura casi vacía que contiene únicamente
<div id="root"></div> - descarga el paquete de JavaScript
- lo analiza y ejecuta
- los componentes se ejecutan, construyen el DOM y finalmente muestran el contenido
En una conexión 3G lenta o en un teléfono con poca potencia, el usuario ve una página en blanco durante toda esta secuencia. Esto es todo con lo que tiene que trabajar el navegador al principio:
<!-- 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>
Cuando los errores dejan de ser tolerados
El analizador de HTML es famosamente tolerante. Úntele marcado defectuoso como este:
<div>
<p>Unclosed paragraph
<div>Nested incorrectly</b>
</div>
y no fallará. El algoritmo de análisis cierra y anida nuevamente los elementos según reglas de recuperación bien definidas y muestra el contenido de todos modos.
React es mucho menos indulgente en tiempo de ejecución. Si la renderización lanza un error, por ejemplo porque una expresión como {user.profile.name} intenta acceder a una propiedad de undefined, React desmonta todo el árbol cuando no hay un límite de error que lo capture, dejando una pantalla en blanco. React no incluye por defecto un componente <ErrorBoundary> listo para usar; debes crear uno como componente de clase (o utilizar una librería pequeña) y colocarlo intencionalmente alrededor de las áreas riesgosas.
Formularios y eventos
Los formularios HTML han gestionado la entrada, la validación y el envío de forma nativa desde los inicios de la web. Los patrones típicos de JSX suelen reemplazar esas funcionalidades primitivas por implementaciones en JavaScript.
Entradas controladas versus nativas
Un simple <input> mantiene su propio estado. Al escribir, se actualiza inmediatamente el búfer interno del navegador, sin necesidad de scripts. En cambio, el patrón idiomático de React convierte la entrada en controlada, de modo que el estado de React se convierte en la fuente de verdad:
// 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)}
/>
);
}
Ahora, cada tecla presionada pasa por el sistema de eventos, modifica el estado, vuelve a ejecutar la función del componente, realiza la reconciliación y luego devuelve el valor al DOM. Esto funciona bien para un componente pequeño. Pero cuando el hilo principal está ocupado procesando datos o realizando animaciones intensivas, o cuando la entrada se encuentra dentro de un gran árbol de componentes que se vuelve a renderizar junto con ella, los usuarios pueden observar que los caracteres aparecen de forma notablemente tardía después de escribirlos.
Eventos sintéticos
React envuelve los eventos nativos del navegador en su propio sistema de eventos sintéticos, inicialmente para resolver las incoherencias entre navegadores antiguos. Esta abstracción genera ciertas fricciones ocultas:
- la propagación a través del árbol de React puede diferir de la propagación a través de los listeners nativos del DOM, lo que confunde al código que utiliza ambos
Accesibilidad y desviación semántica
JSX puede generar HTML semántico y perfectamente accesible. Sin embargo, los patrones que fomenta tienden a erosionar la semántica con el tiempo.
Sopa de divs provenientes de envoltorios de componentes
Un componente debe devolver un único nodo raíz. Los fragmentos (<Fragment> o <>) resuelven este problema sin crear un DOM adicional, pero muchas bases de código siguen envolviendo los hijos en contenedores <div> por costumbre o para el diseño. La estructura que se pretendía crear es esta:
<!-- What you intended to build -->
<main>
<article>
<h1>Article Title</h1>
<p>Content goes here...</p>
</article>
</main>
Mientras que los componentes envolventes en capas suelen renderizar algo similar a esto:
<!-- 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>
Las capas adicionales hacen que el DOM sea más pesado, complican el diseño con CSS y generan interferencias entre los elementos clave. Los <div> genéricos sin roles suelen ser ignorados por las tecnologías de asistencia, pero las cadenas profundas de envoltorios siguen dificultando la comprensión del marcado y aumentando el riesgo de errores, por ejemplo cuando un envoltorio rompe accidentalmente la estructura de una lista o encabezado.
Comportamiento del teclado que tienes de forma gratuita, hasta que ya no lo tienes
Los elementos interactivos nativos como <button>, <a>, <select> y <details> vienen con comportamientos que a menudo se dan por sentados:
- por defecto son enfocables en el orden de tabulación
- Enter y Espacio los activan automáticamente
Dado que JSX hace que sea sencillo asociar un manejador de clic a cualquier elemento, como en <div onClick={handleClick}>, los equipos construyen regularmente controles personalizados a partir de elementos no semánticos y olvidan los manejadores de teclado, tabIndex y los roles ARIA que esos elementos nativos proporcionan automáticamente. Nuestra visión general de errores ocultos de componentes React aborda más de estas trampas.
Estándares, interoperabilidad y dependencia
HTML es un estándar abierto mantenido por WHATWG, con la participación histórica de W3C. Las páginas escritas en 1997 siguen funcionando en los navegadores actuales. En contraste, JSX no es un estándar web; cuenta con una especificación informal y es compatible con varias bibliotecas, como React, Preact y Solid, pero cada uso depende de un compilador y del entorno en tiempo de ejecución al que apunta la compilación. Se trata de una forma menos estricta de encadenamiento que un formato propietario, pero sigue siendo un encadenamiento.
Elementos personalizados y el modelo de componentes propio de la plataforma
Los navegadores ya incluyen un modelo de componentes: Elementos Personalizados más Shadow DOM. El HTML simple los utiliza directamente:
<user-avatar src="avatar.jpg" size="large"></user-avatar>
Historialmente, React manejó mal los elementos personalizados por dos razones:
- pasaba todas las propiedades a etiquetas desconocidas en minúsculas como atributos de texto, por lo que los objetos y arrays no podían ser transmitidos como propiedades del elemento
new CustomEvent('user-select'), no se asociaban a una propiedad como onUserSelect, lo que obligaba a utilizar componentes de envoltura o listeners manuales a través de refsReact 19 abordó gran parte de esto al establecer propiedades en los elementos personalizados cuando el elemento las define y al soportar controladores de eventos personalizados; por lo tanto, verifique qué versión de React utiliza antes de asumir que estas limitaciones siguen aplicándose.
Portabilidad del marcado
Un sistema de diseño escrito en HTML y CSS estándar puede ser utilizado desde cualquier lugar: WordPress, Django, Ruby on Rails, plantillas de Go, Vue, Angular, Svelte o páginas estáticas simples. Un sistema de diseño escrito como componentes JSX está vinculado al ecosistema de JavaScript; usarlo desde un backend que no sea JavaScript requiere un servicio de renderizado de Node.js o una segunda implementación de cada componente.
HTML y JSX lado a lado
Resumiendo las compensaciones:
- Ejecución: HTML es analizado de forma nativa por el navegador; JSX se compila en llamadas a funciones de JavaScript que se ejecutan en tiempo de ejecución.
- Herramientas: HTML necesita un editor y un navegador; JSX requiere un compilador, un bundler, un gestor de paquetes y un entorno de construcción.
- Sintaxis: HTML es insensible a mayúsculas y minúsculas y es tolerante; JSX es sensible a mayúsculas y minúsculas, utiliza propiedades con nombres diferentes y requiere cierres al estilo XML.
- Renderizado: HTML envía flujos y los dibuja de forma incremental; el JSX renderizado en el cliente espera a que se descargue y ejecute el paquete.
- Errores: HTML se recupera de marcado mal formado; un error de renderizado no capturado desmonta el árbol de React.
- Formularios: los elementos de entrada nativos mantienen su propio estado; los elementos de entrada controlados se vuelven a renderizar con cada tecla pulsada.
Recuperar lo que se perdió
Nada de esto aboga por abandonar el desarrollo basado en componentes. Aboga por ser deliberados respecto a dónde vale la pena invertir esfuerzo en abstracciones. El ecosistema ha evolucionado exactamente en esta dirección, con patrones que restauran la velocidad y resistencia del HTML nativo mientras mantienen un modelo de creación declarativo.
Elija la renderización server-first
- React Server Components se renderizan en el servidor y no envían código JavaScript de componentes al cliente para las partes que no son interactivas.
- Astro utiliza una arquitectura basada en islas: por defecto, las páginas son HTML estático, y solo se cargan widgets interactivos aislados.
- Qwik reemplaza este proceso de carga por la posibilidad de reanudación, serializando el estado de la aplicación en el HTML para que el código se ejecute únicamente cuando el usuario interactúa realmente.
Deje que el navegador gestione el estado del formulario
En lugar de reflejar cada tecla pulsada en el estado, permita que el <form> nativo almacene los valores y los lea una sola vez al enviarlo mediante 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>
);
}
La entrada se actualiza a la velocidad nativa, el atributo required proporciona validación integrada, y el componente se renderiza una sola vez en lugar de con cada tecla pulsada. Las versiones recientes de React se basan en la misma idea con acciones de formulario que aceptan directamente FormData.
Mantenga la semántica estricta
Trate JSX como una forma de generar HTML semántico, no como una licencia para apilar contenedores:
- sustituya los envolventes
<div>por fragmentos (<></>) siempre que el envolvente no tenga propósito estilístico - utilice elementos interactivos nativos como
<button>,<dialog>,<details>y<summary>en lugar de widgets creados manualmente - agregue
eslint-plugin-jsx-a11ya la integración continua para que la falta de etiquetas, roles y controladores de teclado impida la compilación
Conclusión
JSX transformó el desarrollo front-end al demostrar que las interfaces se describen mejor como funciones predecibles de los datos, y resolvió problemas reales relacionados con mantener interfaces gráficas grandes y dinámicas sincronizadas con el estado. Sigue siendo una abstracción de JavaScript, no una versión más reciente de HTML. Elegirlo implica sacrificar la transmisión nativa, la creación de contenido sin necesidad de compilación, la tolerancia a errores y la estabilidad a largo plazo de los estándares en favor de la composición y una ergonomía reactiva. A menudo se trata de un buen intercambio. La habilidad técnica radica en saber exactamente qué se está sacrificando y en recurrir a la renderización en servidor, los formularios nativos y los elementos semánticos siempre que sea posible recuperar esas capacidades de forma gratuita.
Lecturas relacionadas
- REST vs GraphQL: Los reales intercambios detrás de cada arquitectura — Explica los problemas específicos que resuelven REST y GraphQL, sus mecanismos internos y los intercambios ocultos que deben considerarse antes de elegir uno para su API.
- Diez errores ocultos en componentes React que ralentizan las aplicaciones modernas — Conozca diez errores comunes en componentes React, desde deficiencias en el HTML semántico hasta la falta de memorización, y las soluciones necesarias para mantener las aplicaciones rápidas, accesibles y sin errores en 2026.