Patrones de Vite
Patrones de herramienta de compilación y servidor de desarrollo para proyectos Vite 8+. Cubre configuración, variables de entorno, configuración de proxy, modo librería, pre-empaquetado de dependencias y errores comunes de producción.
Cuándo usar
- Configurar
vite.config.tsovite.config.js - Configurar variables de entorno o archivos
.env - Configurar el proxy del servidor de desarrollo para backends de API
- Optimizar la salida de compilación (fragmentos, minificación, archivos estáticos)
- Publicar librerías con
build.lib - Solucionar problemas de pre-empaquetado de dependencias o interoperabilidad CJS/ESM
- Depurar HMR, el servidor de desarrollo o errores de compilación
- Elegir u ordenar plugins de Vite
Cómo funciona
- Modo de desarrollo sirve archivos fuente como ESM nativo — sin empaquetado. Las transformaciones ocurren bajo demanda por solicitud de módulo, por eso los arranques en frío son rápidos y el HMR es preciso.
- Modo de compilación usa Rolldown (v7+) o Rollup (v5–v6) para empaquetar la aplicación para producción con tree-shaking, división de código y minificación basada en Oxc.
- Pre-empaquetado de dependencias convierte dependencias CJS/UMD a ESM una vez mediante esbuild y almacena en caché el resultado en
node_modules/.vite, por lo que los arranques posteriores omiten el trabajo. - Plugins comparten una interfaz unificada en desarrollo y compilación — el mismo objeto plugin funciona tanto para las transformaciones bajo demanda del servidor de desarrollo como para el pipeline de producción.
- Variables de entorno se insertan estáticamente en tiempo de compilación. Las variables prefijadas con
VITE_se convierten en constantes públicas en el bundle; todo lo que no tenga prefijo es invisible para el código del cliente.
Ejemplos
Estructura de Configuración
Configuración Básica
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
})
Configuración Condicional
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) // solo prefijo VITE_ (seguro)
return {
plugins: [react()],
server: command === 'serve' ? { port: 3000 } : undefined,
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
Opciones Clave de Configuración
| Clave | Valor por defecto | Descripción |
|---|---|---|
root | '.' | Raíz del proyecto (donde vive index.html) |
base | '/' | Ruta base pública para activos desplegados |
envPrefix | 'VITE_' | Prefijo para variables de entorno expuestas al cliente |
build.outDir | 'dist' | Directorio de salida |
build.minify | 'oxc' | Minificador ('oxc', 'terser', o false) |
build.sourcemap | false | true, 'inline', o 'hidden' |
Plugins
Plugins Esenciales
La mayoría de las necesidades de plugins están cubiertas por un puñado de paquetes bien mantenidos. Recurre a estos antes de escribir los tuyos propios.
| Plugin | Propósito | Cuándo usar |
|---|---|---|
@vitejs/plugin-react-swc | React HMR + Fast Refresh vía SWC | Por defecto para aplicaciones React (más rápido que la variante Babel) |
@vitejs/plugin-react | React HMR + Fast Refresh vía Babel | Solo si necesitas plugins de Babel (emotion, decoradores MobX) |
@vitejs/plugin-vue | Soporte para SFC de Vue 3 | Aplicaciones Vue |
vite-plugin-checker | Ejecuta tsc + ESLint en un hilo de trabajo con superposición HMR | Cualquier aplicación TypeScript — Vite NO verifica tipos durante vite build |
vite-tsconfig-paths | Respeta los alias paths de tsconfig.json | Cada vez que ya tengas alias en tsconfig.json |
vite-plugin-dts | Emite archivos .d.ts en modo librería | Publicar librerías TypeScript |
vite-plugin-svgr | Importa SVGs como componentes React | Aplicaciones React que usan SVGs como componentes |
rollup-plugin-visualizer | Informe de treemap/sunburst del bundle | Auditorías periódicas del tamaño del bundle (usa enforce: 'post') |
vite-plugin-pwa | PWA + Workbox sin configuración | Aplicaciones que funcionan sin conexión |
Advertencia crítica: vite build transpila pero NO verifica tipos. Los errores de tipo llegan silenciosamente a producción a menos que agregues vite-plugin-checker o ejecutes tsc --noEmit en CI.
Creación de Plugins Personalizados
Crear plugins es raro — la mayoría de las necesidades están cubiertas por plugins existentes. Cuando necesites uno, comienza en línea en vite.config.ts y solo extráelo si se reutiliza.
// vite.config.ts — plugin en línea mínimo
function myPlugin(): Plugin {
return {
name: 'my-plugin', // requerido, debe ser único
enforce: 'pre', // 'pre' | 'post' (opcional)
apply: 'build', // 'build' | 'serve' (opcional)
transform(code, id) {
if (!id.endsWith('.custom')) return
return { code: transformCustom(code), map: null }
},
}
}
Hooks clave: transform (modificar fuente), resolveId + load (módulos virtuales), transformIndexHtml (inyectar en HTML), configureServer (agregar middleware de desarrollo), hotUpdate (HMR personalizado — reemplaza el obsoleto handleHotUpdate en v7+).
Módulos virtuales usan la convención de prefijo \0 — resolveId devuelve '\0virtual:my-id' para que otros plugins lo omitan. El código de usuario importa 'virtual:my-id'.
Para la API completa de plugins, consulta vite.dev/guide/api-plugin. Usa vite-plugin-inspect durante el desarrollo para depurar el pipeline de transformación.
API HMR
Los plugins de framework (@vitejs/plugin-react, @vitejs/plugin-vue, etc.) manejan HMR automáticamente. Recurre a import.meta.hot directamente solo al construir almacenes de estado personalizados, herramientas de desarrollo o utilidades independientes del framework que necesiten persistir estado entre actualizaciones.
// src/store.ts — HMR manual para un módulo simple
if (import.meta.hot) {
// Persistir estado entre actualizaciones (debe MUTAR, nunca reasignar .data)
import.meta.hot.data.count = import.meta.hot.data.count ?? 0
// Limpiar efectos secundarios antes de que el módulo sea reemplazado
import.meta.hot.dispose((data) => clearInterval(data.intervalId))
// Aceptar las propias actualizaciones de este módulo
import.meta.hot.accept()
}
Todo el código import.meta.hot se elimina mediante tree-shaking de las compilaciones de producción — no se necesita eliminar la guarda.
Variables de Entorno
Vite carga .env, .env.local, .[mode], y .[mode].local en ese orden (el último sobrescribe al anterior); los archivos *.local están en gitignore y están destinados a secretos locales.
Acceso desde el Cliente
Solo las variables prefijadas con VITE_ están expuestas al código del cliente:
import.meta.env.VITE_API_URL // string
import.meta.env.MODE // 'development' | 'production' | custom
import.meta.env.BASE_URL // valor de configuración base
import.meta.env.DEV // booleano
import.meta.env.PROD // booleano
import.meta.env.SSR // booleano
Uso de Env en la Configuración
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()) // solo prefijo VITE_ (seguro)
return {
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
Seguridad
El Prefijo VITE_ NO es un Límite de Seguridad
Cualquier variable prefijada con VITE_ se inserta estáticamente en el bundle del cliente en tiempo de compilación. La minificación, la codificación base64 y la desactivación de sourcemaps NO lo ocultan. Un atacante decidido puede extraer cualquier variable VITE_ del JavaScript enviado.
Regla: Solo valores públicos (URLs de API, características flags, claves públicas) van en variables VITE_. Los secretos (tokens de API, URLs de base de datos, claves privadas) DEBEN residir en el lado del servidor detrás de una API o función serverless.
La Trampa de loadEnv('')
// MALO: pasar '' como tercer argumento carga TODAS las variables de entorno — incluidos secretos del servidor —
// y las hace disponibles para insertar en el código del cliente mediante `define`.
const env = loadEnv(mode, process.cwd(), '')
// BUENO: lista explícita de prefijos
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
Source Maps en Producción
Los source maps de producción filtran tu código fuente original. Desactívalos a menos que los subas a un rastreador de errores (Sentry, Bugsnag) y los elimines localmente después:
build: {
sourcemap: false, // valor por defecto — mantenlo así
}
Lista de Verificación de .gitignore
.env.local,.env.*.local— sobrescrituras de secretos localesdist/— salida de compilaciónnode_modules/.vite— caché de pre-empaquetado (entradas obsoletas causan errores fantasma)
Proxy del Servidor
// vite.config.ts — server.proxy
server: {
proxy: {
'/foo': 'http://localhost:4567', // abreviatura de cadena
'/api': {
target: 'http://localhost:8080',
changeOrigin: true, // necesario para backends con host virtual
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
Para proxy de WebSocket, agrega ws: true a la configuración de la ruta.
Optimización de Compilación
Fragmentos Manuales
// vite.config.ts — build.rolldownOptions
build: {
rolldownOptions: {
output: {
// Forma de objeto: agrupar paquetes específicos
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
},
},
},
}
// Forma de función: dividir por heurística
manualChunks(id) {
if (id.includes('node_modules/react')) return 'react-vendor'
if (id.includes('node_modules')) return 'vendor'
}
Rendimiento
Evitar Archivos Barril
Los archivos barril (index.ts que reexportan todo de un directorio) obligan a Vite a cargar cada archivo reexportado incluso cuando importas un solo símbolo. Esta es la principal ralentización del servidor de desarrollo señalada por los documentos oficiales.
// MALO — importar una utilidad obliga a Vite a cargar todo el barril
import { slash } from '@/utils'
// BUENO — importación directa, solo se carga el archivo
import { slash } from '@/utils/slash'
Ser Explícito con las Extensiones de Importación
Cada extensión implícita obliga a hasta 6 comprobaciones del sistema de archivos mediante resolve.extensions. En bases de código grandes, esto se acumula.
// MALO
import Component from './Component'
// BUENO
import Component from './Component.tsx'
Reduce allowImportingTsExtensions en tsconfig.json y resolve.extensions solo a las extensiones que realmente uses.
Calentar Rutas de Alto Tráfico
server.warmup.clientFiles pre-transforma entradas conocidas de alto tráfico antes de que el navegador las solicite — eliminando la cascada de solicitudes de carga en frío en aplicaciones grandes.
// vite.config.ts
server: {
warmup: {
clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
},
}
Perfilado de Servidores de Desarrollo Lentos
Cuando vite dev se siente lento, comienza con vite --profile, interactúa con la aplicación, luego presiona p+enter para guardar un archivo .cpuprofile. Cárgalo en Speedscope para encontrar qué plugins están consumiendo tiempo — generalmente los hooks buildStart, config, o configResolved en plugins de la comunidad.
Modo Librería
Al publicar un paquete npm, usa build.lib. Dos trampas importan más que los detalles de configuración:
- Los tipos no se emiten — agrega
vite-plugin-dtso ejecutatsc --emitDeclarationOnlypor separado. - Las dependencias pares DEBEN externalizarse — las pares no listadas se empaquetan en tu librería, causando errores de duplicación en tiempo de ejecución en los consumidores.
// vite.config.ts
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `my-lib.${format}.js`,
},
rolldownOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'], // cada dependencia par
},
}
Externalizaciones SSR
Las configuraciones simples de createServer({ middlewareMode: true }) son territorio de autores de frameworks. La mayoría de las aplicaciones deberían usar Nuxt, Remix, SvelteKit, Astro o TanStack Start. Lo que ajustarás como usuario de framework es la configuración de externalizaciones cuando las dependencias fallen en SSR:
// vite.config.ts — opciones ssr
ssr: {
external: ['node-native-package'], // mantener como require() en el bundle SSR
noExternal: ['esm-only-package'], // forzar empaquetado en la salida SSR (soluciona la mayoría de errores SSR)
target: 'node', // 'node' o 'webworker'
}
Pre-empaquetado de Dependencias
Vite pre-empaqueta dependencias para convertir CJS/UMD a ESM y reducir el número de solicitudes.
// vite.config.ts — optimizeDeps
optimizeDeps: {
include: [
'lodash-es', // forzar pre-empaquetado de dependencias pesadas conocidas
'cjs-package', // dependencias CJS que causan problemas de interoperabilidad
'deep-lib/components/**', // glob para importaciones profundas
],
exclude: ['local-esm-package'], // debe ser ESM válido si se excluye
force: true, // ignorar caché, re-optimizar (depuración temporal)
}
Errores Comunes
Desarrollo no Coincide con Compilación
Desarrollo usa esbuild/Rolldown para transformaciones; compilación usa Rolldown para empaquetado. Las librerías CJS pueden comportarse de manera diferente entre ambos. Siempre verifica con vite build && vite preview antes de desplegar.
Fragmentos Obsoletos Después del Despliegue
Nuevas compilaciones producen nuevos hashes de fragmentos. Los usuarios con sesiones activas solicitan nombres de archivo antiguos que ya no existen. Vite no tiene una solución incorporada. Mitigaciones:
- Mantén los archivos antiguos de
dist/assets/vivos durante una ventana de despliegue - Captura errores de importación dinámica en tu enrutador y fuerza una recarga de página
Docker y Contenedores
Vite se vincula a localhost por defecto, lo que es inalcanzable desde fuera de un contenedor:
// vite.config.ts — configuración Docker/contenedor
server: {
host: true, // enlazar 0.0.0.0
hmr: { clientPort: 3000 }, // si está detrás de un proxy inverso
}
Acceso a Archivos en Monorepos
Vite restringe el servicio de archivos a la raíz del proyecto. Los paquetes fuera de la raíz están bloqueados:
// vite.config.ts — acceso a archivos en monorepo
server: {
fs: {
allow: ['..'], // permitir directorio padre (raíz del espacio de trabajo)
},
}
Anti-Patrones
// MALO: establecer envPrefix en '' expone TODAS las variables de entorno (incluidos secretos) al cliente
envPrefix: ''
// MALO: asumir que require() funciona en modo ESM sin configuración explícita
// MALO: no externalizar dependencias pares en modo librería