El problema: el navegador no entiende tu código
El código que escribes —TypeScript, JSX, sintaxis moderna— no es el que un navegador sabe ejecutar, y antes de ESM ni siquiera había módulos. Esta es la doble raíz del build: transformar y servir.
El código que escribes en 2026 —TypeScript con anotaciones de tipo, JSX que parece HTML incrustado en JavaScript, sintaxis que tu editor entiende antes de que ningún motor la implemente— no es el código que un navegador sabe ejecutar. Entre lo que es cómodo escribir y lo que es posible ejecutar hay una brecha, y todo el edificio del build existe para cerrarla: transformar tu fuente en algo que el motor acepte y servirlo de una forma que el navegador cargue.
- Entender por qué el motor de un navegador no ejecuta TypeScript ni JSX.
- Comprender qué significaba no tener un sistema de módulos antes de ESM.
- Ver por qué abrir el archivo no basta y hace falta servir por HTTP.
- Situar la transformación y el servido como las dos raíces del build.
La brecha entre lo que escribes y lo que se ejecuta
Un navegador incorpora un motor de JavaScript —V8 en Chrome, JavaScriptCore en Safari, SpiderMonkey en Firefox— que implementa el estándar ECMAScript y las APIs web. Ese motor no implementa TypeScript, ni JSX, ni la mayoría de propuestas de sintaxis que aún no han llegado al estándar. Todo lo que caiga fuera de ese contrato es, literalmente, un error de sintaxis.
TypeScript es el caso más nítido: los tipos son una ficción de tiempo de compilación. El motor jamás ha visto una anotación de tipo, y una línea como const edad: number = 30 lo detiene en seco. Para que ese archivo se ejecute, los tipos deben borrarse —un proceso llamado type stripping— hasta dejar JavaScript puro.
// Lo que escribes: TypeScript con tipos
interface Usuario {
id: number;
nombre: string;
}
function saludar(u: Usuario): string {
return `Hola ${u.nombre}`;
}
// Lo que el motor necesita: los tipos, borrados
function saludar(u) {
return `Hola ${u.nombre}`;
}
JSX va un paso más allá: no es JavaScript con algo de más, es sintaxis que JavaScript no define en absoluto. Escribir <button> dentro de una expresión no significa nada para el motor; hay que convertirlo en llamadas a funciones ordinarias antes de que exista.
// JSX: parece marcado, pero no es JavaScript valido
const boton = <button onClick={enviar}>Enviar</button>;
// Tras la transformacion: llamadas a una funcion normal
const boton = jsx("button", { onClick: enviar, children: "Enviar" });
TypeScript
Los tipos son ficción de tiempo de compilación. El motor nunca los ha visto: hay que borrarlos para que quede JavaScript ejecutable.
JSX
No es JavaScript con extras, es sintaxis ajena al lenguaje. Debe convertirse en llamadas a funciones antes de poder existir.
Sintaxis nueva
Decoradores, campos privados y propuestas en curso van por delante de lo que cada motor ha enviado, y hay que rebajarlos al target.
A esto se suma la sintaxis que va por delante de los motores: decoradores, campos privados, propuestas en fase experimental. Aunque en 2026 los navegadores son evergreen y convergen rápido, sigue existiendo el desfase entre lo que el comité TC39 aprueba y lo que cada motor ha enviado. Alguien tiene que decidir a qué nivel de sintaxis apuntar —el target— y rebajar el resto.
Conviene distinguir dos operaciones que suelen confundirse. El type stripping solo elimina las anotaciones y deja el resto intacto: es rapidísimo y no comprueba nada. La compilación de TypeScript, en cambio, valida los tipos y puede además rebajar sintaxis. Node 24 y herramientas como esbuild u Oxc borran tipos a velocidad de vértigo sin verificarlos; la verificación queda para tsc o para tu editor. Ejecutar y comprobar se han separado en dos tareas distintas, y esa separación es hoy la norma.
El problema de los módulos
Antes de 2015 el navegador no tenía ningún concepto de módulo. Cada etiqueta <script> se ejecutaba en el mismo ámbito global —el objeto window— y compartía sus variables con todas las demás. No había encapsulación ni importaciones: solo un espacio de nombres gigante y compartido en el que cada script podía pisar a cualquier otro.
<!-- El mundo sin modulos: todo cuelga de window y el orden manda -->
<script src="jquery.js"></script>
<script src="utilidades.js"></script>
<script src="app.js"></script>
Ese orden no es cosmético: es una dependencia implícita y frágil. Si app.js usa algo definido en utilidades.js, debe cargarse después, y nadie lo declara en ninguna parte —lo sabes o lo rompes—. Dos librerías que definan una variable global con el mismo nombre colisionan en silencio. Para defenderse, la comunidad inventó patrones como la función anónima autoejecutada, que fabricaba un ámbito privado a mano.
ESM —los módulos estándar de ECMAScript, con import y export— resolvió esto en el propio lenguaje: ámbito por archivo, dependencias explícitas y análisis estático. Los navegadores lo soportan mediante <script type="module">. Pero queda un cabo suelto decisivo:
// El navegador resuelve esto: una ruta relativa con extension
import { suma } from "./matematicas.js";
// El navegador NO sabe resolver esto: un "bare specifier"
import { debounce } from "lodash-es";
Un bare specifier como lodash-es no es una URL ni una ruta. El navegador no tiene idea de qué es node_modules ni de cómo convertir ese nombre en un archivo concreto. Alguien debe traducir el nombre del paquete a una dirección real: esa traducción es la resolución de módulos, y es una de las tareas centrales que hereda cualquier herramienta de build.
Servir no es abrir el archivo
Supongamos que ya tienes JavaScript válido y con las rutas resueltas. Todavía no puedes hacer doble clic en el index.html. El protocolo file:// bloquea los módulos ESM por seguridad, rompe fetch y no aplica los tipos MIME que el navegador exige para tratar un recurso como módulo. Necesitas un servidor HTTP que entregue cada archivo con su cabecera correcta —text/javascript para los módulos— y que, si quieres una experiencia de desarrollo decente, recargue la página o parchee el módulo en caliente cada vez que guardas. Un servidor de desarrollo mínimo tiene que, al menos:
- Entregar cada módulo con el tipo MIME correcto para que el navegador lo trate como tal.
- Traducir sobre la marcha lo que aún no es JavaScript estándar, antes de enviarlo.
- Vigilar el sistema de archivos y avisar al navegador en cuanto algo cambia.
Aquí aparecen las dos raíces del build, entrelazadas: transformar lo que escribiste en algo ejecutable y servirlo de forma que el navegador lo cargue y tú puedas iterar rápido.
flowchart LR subgraph Escribes A[TypeScript] B[JSX] C[import lodash-es] end subgraph Transformar D[Borrar tipos] E[JSX a funciones] F[Resolver modulos] end subgraph Servir G[Servidor HTTP] H[Tipos MIME] I[Recarga en vivo] end A --> D B --> E C --> F D --> G E --> G F --> G G --> I style A fill:#f38ba8,color:#11111b style G fill:#a6e3a1,color:#11111b
Toda la disciplina del build se explica por una tensión de fondo: lo que hace productivo a quien programa y lo que hace ejecutable a un navegador tiran en direcciones opuestas. Tú quieres tipos que atrapen errores, una sintaxis expresiva que parezca marcado y organizar el código en cientos de archivos pequeños con nombres claros. El motor quiere JavaScript estándar, sin tipos, y una forma inequívoca de encontrar cada dependencia. Ninguna de las dos partes va a ceder: los tipos no se ejecutarán nunca y tú no vas a escribir document.createElement a mano para cada botón. La herramienta de build es el traductor que vive entre ambos mundos, y por eso se descompone siempre en las mismas dos fuerzas —transformar el código y entregarlo—. Cuando comprendes que el build no es un capricho de la moda sino la consecuencia inevitable de esa brecha, dejas de verlo como un obstáculo y empiezas a verlo como lo que es: la infraestructura que te permite escribir para un humano y ejecutar para una máquina. Todo lo demás de este track —bundlers, dev servers, caching— son refinamientos de esas dos ideas.
- Crea un archivo
demo.tscon una anotación de tipo y cárgalo en el navegador dentro de un<script type="module">: observa el error de sintaxis exacto que devuelve. - Añade dos etiquetas
<script>clásicas que definan una misma variable global y comprueba cómo la segunda pisa a la primera sin avisar. - Escribe un
importde un bare specifier comolodash-essin herramientas y lee el mensaje del navegador al no poder resolverlo. - Sirve esa misma carpeta con un servidor HTTP mínimo y observa qué deja de fallar: esa diferencia es media razón de ser del build.