wandres.dev
DEV VS BUILD · las dos mitades

Depurar diferencias dev/prod: reproducir el bug en local

Un bug que solo aparece en producción es, ante todo, un problema de método: hay que dejar de depurar el dev server y reproducir el artefacto real en tu máquina. Las herramientas son `vite build` más `vite preview` para servir el bundle de verdad, `vite build --debug` y los source maps para ver la tubería por dentro, y una bisección disciplinada de las optimizaciones hasta acorralar la causa. Cómo colapsar la distancia dev/prod hasta tener el bug en las manos.

⏱ 15 min

Cierra el nivel el problema más práctico de todos: algo falla en producción y no lo reproduces en vite. La tentación es depurar en el dev server, pero ya sabes por qué es inútil —el dev es un intérprete permisivo que no ejerce las transformaciones donde el bug vive—. La respuesta no es una herramienta mágica sino una disciplina: reproducir el artefacto real en local, aislar cuál de las optimizaciones del build lo provoca, y usar los source maps para volver del código minificado a tu fuente. Depurar dev/prod es, en el fondo, el arte de acortar la distancia hasta que el bug quepa en tu máquina.

🎯 Al terminar esta lección sabrás
  • Reproducir un bug de solo-producción en local con vite build y vite preview.
  • Usar vite build --debug y los source maps para inspeccionar la tubería.
  • Aislar la causa entre minificación, tree shaking, define y entorno.
  • Construir un flujo disciplinado de comparación dev/prod.

preview: servir el artefacto real

El primer movimiento no admite atajos: compila y sirve el bundle. vite build genera el artefacto y vite preview lo sirve como un servidor estático, ejecutando el mismo código minificado y optimizado que irá a producción. Es la forma más rápida de partir el problema en dos: si el bug se reproduce bajo vite preview, vive en el build; si no, vive fuera —en tu servidor real, tus cabeceras, tu CDN o tus variables de entorno—.

# El primer diagnostico: reproducir el artefacto real en local
vite build            # genera dist con el codigo optimizado
vite preview          # sirve ese dist como lo veria produccion

Esa bifurcación te ahorra horas. Un porcentaje enorme del tiempo perdido depurando “en producción” se va en no haber determinado primero de qué lado de la frontera está el fallo. vite preview responde esa pregunta en segundos. Eso sí, con una advertencia que la documentación repite: vite preview no es un servidor de producción, solo una previsualización del artefacto; hay diferencias de entorno que no captura, y de esas nos ocupamos al final.

Conviene entender por qué vite preview es fiel donde importa. Sirve exactamente los bytes de dist/ —el mismo JavaScript minificado, los mismos chunks, los mismos assets hasheados que subirás—, así que cualquier bug introducido por una transformación del build se manifiesta idéntico. Lo que no replica es la capa de infraestructura: no es tu Nginx, ni tu CDN, ni tu función serverless. Por eso es un simulador perfecto del artefacto y un simulador nulo del entorno, y saber esa frontera es saber qué clase de bug puede o no puede reproducir.

💡
Antes de tocar nada, parte el problema en dos

La primera pregunta ante un bug de producción no es “por qué falla”, sino “se reproduce en vite preview”. Con respuesta afirmativa, el universo de causas se reduce al pipeline de build. Con respuesta negativa, ni mires el bundle: el problema está en el entorno de despliegue. Contestar esa pregunta antes de formular hipótesis es lo que separa una depuración de una hora de una de un día.

Aislar la causa con interruptores

Si el bug se reproduce en vite preview, ahora bisecas las optimizaciones para ver cuál lo introduce. Cada transformación del build tiene un interruptor, y apagarlos de uno en uno acorrala la causa. Empieza por la minificación: si el bug desaparece con build.minify en falso, tienes un problema de minificación —a menudo una suposición sobre nombres de variables o sobre orden que el minificador reescribió—.

# Bisecar el pipeline apagando optimizaciones y leyendo la tuberia
vite build --minify false     # descarta o confirma la minificacion
vite build --sourcemap        # emite mapas para volver a tu fuente
vite build --debug            # o DEBUG=vite:* para ver el pipeline por dentro

Si con la minificación apagada el bug persiste, sigue bajando por la lista de sospechosos: revisa las sustituciones de define —recuerda que reemplaza tokens por literales de forma textual—, comprueba si un export que usas por reflexión fue víctima del tree shaking, y confirma que ninguna variable de entorno que esperabas quedó fuera por no llevar el prefijo VITE_. vite build --debug, o el entorno DEBUG=vite:*, imprime los registros internos de resolución y transformación que te dicen qué decidió el motor en cada módulo.

El orden de sospecha, de lo más a lo menos probable:

  • Minificación: apágala con build.minify: false y culpa al renombrado o al colapso de código.
  • define: revisa que cada token se sustituyó por el literal que esperabas.
  • Tree shaking: busca en el bundle lo que creías presente y comprueba si desapareció.
  • Entorno: confirma que ninguna variable quedó fuera por faltarle el prefijo VITE_.

La bisección se apoya en una técnica humilde pero infalible: leer el bundle. Con la minificación apagada, el artefacto es legible, y un simple grep sobre dist/ te dice si un símbolo que creías presente sobrevivió al tree shaking o si un define sustituyó un token por el literal que esperabas. No hay que adivinar qué hizo el build cuando puedes abrir su salida y verla.

# Leer el artefacto directamente para confirmar una hipotesis
grep -r "miFuncionCritica" dist/     # sigue en el bundle o la borro el tree shaking
grep -r "import.meta.env" dist/      # quedo algun token sin sustituir por define
🔇

Minificación

Apágala con build.minify: false. Si el bug se va, la causa está en el renombrado o el colapso de código del minificador.

🌿

Tree shaking

Sospecha si algo usado por reflexión desapareció. Búscalo en el bundle: si no está, el análisis estático lo creyó muerto.

🔤

define y entorno

Verifica las sustituciones textuales y el prefijo VITE_. Una constante mal resuelta o una env ausente rompen solo en build.

Del stack minificado a tu fuente

Cuando el error salta en el bundle, su stack trace apunta a una sola línea ilegible. Los source maps deshacen esa ofuscación: compilando con build.sourcemap en verdadero y abriendo el artefacto en vite preview, las DevTools mapean el error de vuelta a tu TypeScript original, con nombres y números de línea reales. Sin ellos, depurar el bundle es leer jeroglíficos; con ellos, depuras producción como si fuera tu fuente.

Para un stack trace capturado en producción de verdad —no en preview— guarda los source maps de ese build y decodifícalo con una herramienta que los aplique, de modo que la traza minificada se traduzca a las posiciones de tu código. Es la contrapartida de campo del mismo principio: el artefacto es la verdad, y los source maps son el puente que te devuelve de esa verdad optimizada a la fuente que puedes leer y editar.

Dos cautelas al trabajar con source maps:

  • Actívalos con build.sourcemap solo cuando depures: pesan y pueden exponer tu fuente si los publicas sin querer.
  • Un mapa desalineado con su bundle te lleva a líneas equivocadas; regenera siempre el bundle y su mapa a la vez.
// Decodificar una posicion minificada usando el source map del build
import { SourceMapConsumer } from "source-map";
const consumer = await new SourceMapConsumer(mapaDelBuild);
const original = consumer.originalPositionFor({ line: 1, column: 84213 });
// original.source y original.line apuntan a tu codigo real

Diferencias que preview no captura

Queda el caso en que el bug no se reproduce en vite preview: entonces no está en el build, sino en el entorno. Aquí viven las variables sin el prefijo VITE_ que tu servidor real inyecta y preview no, un base distinto al desplegar bajo un subdirectorio, las cabeceras y el comportamiento de caché de tu CDN, o un modo distinto. Recuerda que el modo es un eje aparte del comando: vite build --mode staging produce otro conjunto de variables, y reproducir el modo correcto en local a veces es la pieza que faltaba.

# Acercar el entorno local al real: modo y ruta base
vite build --mode production        # cargar el .env.production correcto
vite preview --base /mi-app/        # reproducir un despliegue en subdirectorio

El patrón mental es el de una escalera de fidelidad: vite preview con el modo por defecto es el escalón más bajo; ajustar el modo, la base y las variables te sube peldaños hacia el entorno real; y solo cuando ni así reproduces, el problema es genuinamente de infraestructura y toca mirar logs del servidor, cabeceras del CDN o la configuración del despliegue. Cada peldaño que subes descarta una familia de causas.

Lo que vite preview no reproduce del entorno real:

  • Variables de servidor sin prefijo VITE_ que solo existen en tu plataforma de despliegue.
  • Un base distinto al servir bajo un subdirectorio o un dominio propio.
  • Las cabeceras, la compresión y la política de caché de tu CDN.
  • El modo activo, si tu despliegue usa uno distinto de production.
flowchart TD
P[Bug solo en produccion] --> B[vite build y vite preview]
B --> Q{Se reproduce en preview}
Q -->|si| M[Vive en el pipeline de build]
M --> M1[Apaga minify]
M --> M2[Revisa define y env]
M --> M3[Sospecha del tree shaking]
Q -->|no| E[Vive en el entorno o servidor]
E --> E1[Variables sin prefijo VITE]
E --> E2[base cabeceras o CDN]
style P fill:#f38ba8,color:#11111b
style M fill:#f9e2af,color:#11111b
style E fill:#89b4fa,color:#11111b
Un bug que no reproduces es un bug que no puedes arreglar

Toda esta lección se reduce a un axioma que trasciende a Vite: un bug que no reproduces es un bug que no puedes arreglar, solo adivinar. La razón por la que los fallos de producción intimidan no es que sean más difíciles, sino que ocurren lejos —en un artefacto que no ves, en un servidor al que no entras, tras optimizaciones que tu dev server nunca aplicó—. El trabajo del ingeniero no es adivinar a distancia, sino colapsar esa distancia hasta que el bug quepa en su máquina, y toda la caja de herramientas de este nivel existe para eso. vite build más vite preview traen el artefacto optimizado a tu local y parten el problema en build frente a entorno, que es la primera y más rentable de todas las divisiones. Los interruptores de optimización —minify, la inspección del tree shaking, la revisión de define— bisecan el pipeline hasta señalar la transformación culpable. Los source maps te devuelven del código ofuscado a la fuente legible. Y --debug abre la caja negra del motor para que leas sus decisiones. Ninguna es mágica; todas comparten la misma filosofía: convertir lo remoto e invisible en local y observable. Interioriza el reflejo —ante cualquier “solo pasa en producción”, tu primer comando es vite build seguido de vite preview, no otra sesión inútil en el dev server— y habrás cerrado el círculo de las dos naturalezas. Empezaste sabiendo que dev y build divergen; terminas con el método para que esa divergencia deje de esconderte nada.

⚔️ Acorrala un bug de producción
  1. Fuerza un fallo que dependa de la minificación —por ejemplo, código que asuma el nombre de una función— y reprodúcelo con vite build y vite preview.
  2. Apágalo con vite build --minify false y confirma que el bug se desvanece: has localizado la fase culpable.
  3. Compila con build.sourcemap activo y verifica que, al saltar el error en preview, las DevTools te llevan a tu fuente original.
  4. Lanza vite build --debug y localiza en los registros la decisión de resolución o transformación de un módulo concreto.
  5. Provoca un bug que vite preview no reproduzca —una variable sin prefijo VITE_— y demuestra que vive en el entorno, no en el build.