wandres.dev
MINIFICACIÓN · terser, esbuild, oxc

Source maps: del código minificado al original

Un source map es un archivo JSON que traza una correspondencia posición a posición entre el código minificado que se ejecuta y el código original que escribiste. Cómo codifica ese mapa las posiciones con VLQ, cómo lo encuentra el navegador, y los tres tipos —inline, external y hidden— que se diferencian solo en quién tiene permiso para seguir el puntero.

⏱ 15 min

Tras minificar, tu código es un amasijo de una sola línea con variables llamadas a, b y o: imposible de depurar tal cual. El source map es la pieza que reconcilia las dos exigencias en conflicto —enviar algo diminuto y, a la vez, poder leerlo cuando algo falla—. Es un archivo aparte, con extensión .map, que guarda una correspondencia exacta entre cada posición del código generado y la posición del código fuente de la que salió. No cambia lo que se ejecuta ni un byte; es una capa de indirección pura que las herramientas de desarrollo siguen hacia atrás para mostrarte tu TypeScript original en lugar del engrudo minificado.

🎯 Al terminar esta lección sabrás
  • Conocer la estructura de un source map v3 y qué guarda cada uno de sus campos.
  • Entender cómo se codifican las posiciones con VLQ y cómo el navegador encuentra el mapa.
  • Distinguir los tres tipos: inline, external y hidden.
  • Ver el source map como indirección pura que separa lo que se ejecuta de lo que se lee.

Qué hay dentro de un .map

Un source map es un JSON con una forma estándar, la versión 3, que llevan décadas hablando navegadores y herramientas. Sus campos son pocos y cada uno tiene un papel nítido:

{
  "version": 3,
  "file": "app.min.js",
  "sources": ["src/saludar.ts"],
  "sourcesContent": ["export function saludar(n) { ... }"],
  "names": ["saludar", "nombreDeUsuario"],
  "mappings": "AAAA,SAASA,QAAT,CAAkBC..."
}

sources lista los archivos originales; sourcesContent, opcionalmente, incrusta su texto completo para que el depurador no tenga que ir a buscarlos; names guarda los identificadores originales que el mangling destruyó, para poder mostrarte nombreDeUsuario donde el código dice o. Pero el corazón es mappings: una cadena que codifica, para cada posición del archivo generado, de qué línea y columna del original procede.

Los campos, uno a uno:

  • version: siempre 3, el formato vigente que entienden navegadores y herramientas.
  • sources: la lista de archivos originales de los que salió el código generado.
  • sourcesContent: opcional, el texto íntegro de esas fuentes incrustado en el mapa.
  • names: los identificadores originales que el mangling reemplazó por letras.
  • mappings: el núcleo, las posiciones codificadas en VLQ base 64 y en delta.

Esa cadena no es legible por casualidad. Cada segmento codifica hasta cinco números —columna generada, índice de fuente, línea original, columna original e índice de nombre— con VLQ en base 64, y todos van en delta, relativos al segmento anterior. La codificación en diferencias es lo que mantiene el mapa compacto pese a describir cada token: un archivo de miles de líneas cabe en un JSON manejable porque casi nunca guarda una posición absoluta, sino el pequeño salto desde la anterior.

Los cinco números de cada segmento, en orden:

  • Columna generada: dónde empieza el tramo en la línea del archivo minificado.
  • Índice de fuente: a cuál de los sources pertenece el original.
  • Línea y columna originales: el punto exacto del que salió ese tramo.
  • Índice de nombre: opcional, qué identificador de names le corresponde.
ℹ️
Dos cachés, dos capas: el mapa no es el código

Interioriza que el source map vive en un plano distinto del código que describe. El .js es lo que el motor ejecuta; el .map es metadato que solo consumen las herramientas de desarrollo cuando alguien las abre. El navegador ejecuta tu app a plena velocidad sin tocar el mapa jamás; solo lo descarga si abres las DevTools y pide traducir una posición. Por eso un source map, por grande que sea, no ralentiza a tus usuarios: no forma parte del camino de ejecución, es una nota al pie que casi nadie lee.

Cómo el navegador sigue el rastro

Para que la traducción ocurra, el archivo generado debe declarar dónde está su mapa. Lo hace con un comentario especial en su última línea, o con una cabecera HTTP equivalente:

// ...codigo minificado en una sola linea...
//# sourceMappingURL=app.min.js.map

Cuando abres las DevTools, el navegador ve ese comentario, descarga el .map, decodifica los mappings VLQ y construye una tabla de posiciones bidireccional. A partir de ahí, cada vez que pones un breakpoint o lees una traza de pila, traduce la posición del código generado a tu fuente original y te muestra el archivo tal como lo escribiste, con sus nombres y sus líneas. Si el mapa trae sourcesContent, ni siquiera necesita los archivos originales: los lleva dentro.

📝
El enlace también puede ir en una cabecera HTTP

El comentario sourceMappingURL no es la única forma de anunciar el mapa: el servidor puede enviar en su lugar una cabecera SourceMap: con la URL del .map. Es equivalente para el navegador y tiene una ventaja sutil —el .js no lleva ni rastro del mapa en su cuerpo—, aunque en la práctica el comentario sigue siendo lo más común. En ambos casos el mapa solo se descarga si alguien abre las herramientas de desarrollo; nunca forma parte de la carga normal de la página.

Hay un detalle que explica cómo el mapa sobrevive a toda la tubería. Tu código no se minifica de golpe: primero se transpila —de TypeScript a JavaScript—, luego se empaqueta, luego se minifica. Cada etapa produce su propio source map, y las herramientas los componen: el mapa final no apunta al paso anterior, sino todo el camino de vuelta hasta tu .ts original. Es un mapa de un mapa de un mapa, colapsado en uno solo que salta directamente del engrudo minificado a la línea que escribiste.

Cada etapa de la tubería aporta su tramo al mapa final:

  • Transpilar: de tu .ts con tipos y JSX al JavaScript equivalente; primer mapa.
  • Empaquetar: de muchos módulos a unos pocos chunks; el bundler reubica cada línea y actualiza el mapa.
  • Minificar: del chunk legible al chunk apretado; el minificador compone su mapa sobre el anterior.
  • Componer: las herramientas funden los tres tramos en un solo .map que apunta al origen real.
flowchart LR
TS[Tu codigo TypeScript] --> B[Transpilar empaquetar minificar]
B --> JS[app.min.js que se ejecuta]
B --> MAP[app.min.js.map]
MAP -.->|traduce posiciones| TS
JS -.->|apunta a| MAP
style TS fill:#89b4fa,color:#11111b
style JS fill:#f9e2af,color:#11111b
style MAP fill:#a6e3a1,color:#11111b

Tres tipos: inline, external, hidden

Aquí está la decisión práctica. El mismo mapa puede entregarse de tres formas, y la diferencia entre ellas no es su contenido, sino quién puede seguir el puntero.

📥

inline

El mapa entero se incrusta como una URI de datos en base 64 dentro del comentario sourceMappingURL. Autocontenido, sin petición extra, pero infla enormemente el .js. Se usa en desarrollo.

🔗

external

El mapa es un archivo .map aparte, enlazado por el comentario. Solo se descarga si alguien abre las DevTools. Es el estándar en producción cuando decides servir los mapas.

🕵️

hidden

Como external, pero sin el comentario sourceMappingURL. El .map existe, pero nada en el .js desplegado apunta a él. Es la base para subirlo a un servicio de errores sin exponerlo.

El inline es cómodo en desarrollo porque no hay archivo suelto ni petición extra, pero duplica con creces el tamaño del JavaScript, así que en producción es inaceptable. El external es la forma habitual cuando quieres que los mapas estén disponibles: el .js viaja ligero y el .map solo se pide bajo demanda. El hidden es el más sutil y prepara el terreno del siguiente nivel: produce el .map pero corta el hilo que lo anuncia, de modo que puedas quedártelo para depurar en privado sin ofrecérselo al mundo.

La elección del tipo se reduce a dónde estás y a quién quieres dejar traducir:

  • inline en desarrollo: comodidad máxima, sin archivos sueltos; el tamaño no importa en local.
  • external cuando sirves los mapas: el .js viaja ligero y el .map solo se pide al depurar.
  • hidden en producción con servicio de errores: conservas el mapa para ti, no para el público.
  • false si no vas a depurar el build: ni generas ni sirves nada, y ahorras tiempo de build y disco.
// vite.config.ts — cada tipo se pide con un valor distinto
export default defineConfig({
  build: {
    sourcemap: "hidden",   // true = external, "inline" o false tambien valen
  },
});

Generar mapas no es gratis, y conviene saber qué cuesta cada cosa:

  • Tiempo de build: producir el mapa añade trabajo; en inline, además, infla el .js.
  • Disco y despliegue: los .map externos son archivos más que gestionar y, quizá, subir a un servicio.
  • Riesgo de exposición: un mapa servido en abierto entrega tu fuente; por eso hidden existe.
  • Coste en runtime: ninguno, y esto es lo decisivo: el mapa jamás entra en el camino de ejecución.
El source map separa lo que se ejecuta de lo que se lee, y el tipo decide quién sigue el puntero

La belleza del source map es que resuelve una contradicción que parecía irreducible: el código más rápido de ejecutar —diminuto, mangleado, ilegible— y el código más fácil de entender —el original, con nombres y estructura— no pueden ser el mismo archivo, y sin embargo el source map te deja tener los dos a la vez sin pagar por el segundo en tiempo de ejecución. Lo logra con una idea de una elegancia que trasciende el build: una capa de indirección pura. El mapa no altera el programa; se limita a guardar, aparte, la correspondencia entre lo que corre y lo que escribiste, y esa correspondencia solo se consulta cuando hace falta depurar. Fíjate en qué son de verdad los tres tipos una vez lo ves así. No son tres formatos distintos —el JSON es idéntico— sino tres políticas de acceso al mismo puntero. inline mete el mapa en banda, dentro del propio código: cualquiera que tenga el .js tiene el mapa. external lo saca a un archivo enlazado: lo tiene quien decida descargarlo. hidden conserva el mapa pero borra el enlace: lo tiene solo quien ya lo posee por otro canal. Toda la gestión de source maps en producción —la del nivel siguiente— es, en el fondo, elegir con cuidado esa política: quién merece poder traducir tu código de vuelta a su forma legible. Que la misma pieza técnica sirva para depurar cómodamente en desarrollo y para blindar tu fuente en producción, cambiando solo quién sigue el puntero, es lo que convierte al source map en uno de los diseños más limpios de todo el ecosistema.

⚔️ Sigue el mapa a mano
  1. Compila un proyecto con sourcemap: true y localiza el comentario //# sourceMappingURL= al final del .js generado.
  2. Abre el .map, identifica version, sources, names y mappings, y comprueba si trae sourcesContent.
  3. Decodifica a mano el primer segmento de mappings desde VLQ base 64 a sus cinco números y explica a qué posición del original apunta.
  4. Cambia a sourcemap: "inline" y observa la URI de datos gigante dentro del comentario; compara el tamaño del .js con el de la versión external.
  5. Cambia a sourcemap: "hidden", confirma que el .map se genera pero el comentario desaparece, y razona por qué eso es útil de cara a producción.