Laravel 13 + Vite: HMR, build de assets y fuentes autohospedadas
Vite es el bundler de assets por defecto de Laravel desde la versión 9, y en Laravel 13 su plugin oficial además resuelve, optimiza y sirve fuentes autohospedadas con la directiva @fonts. Mientras tanto, la directiva @vite decide sola: conecta con el servidor de desarrollo en local y resuelve los ficheros versionados de producción.
Por qué Laravel usa Vite (y no Mix ni Webpack)
Si has trabajado con Laravel durante años, recordarás la época de Laravel Mix y su configuración en webpack.mix.js. Cada proyecto arrastraba su propia receta de Webpack, con tiempos de compilación que crecían sin control y una experiencia de desarrollo que dependía de plugins de terceros para conseguir algo parecido a una recarga en caliente.
De Laravel Mix a Vite: el cambio de 2022
Con Laravel 9, el equipo decidió sustituir Mix por Vite como herramienta de compilación por defecto. La integración oficial vive en el paquete laravel-vite-plugin, que se encarga de conectar el bundler con la aplicación: registra las entradas, genera el manifest para el backend y expone las directivas de Blade que usas en tus plantillas. Laravel Mix quedó en modo mantenimiento y, desde entonces, los proyectos nuevos nacen directamente con Vite.
Qué ganas: HMR, hashes de contenido y builds en segundos
Vite está construido sobre esbuild y, en las versiones recientes, sobre Rolldown: los builds de producción se resuelven en segundos incluso en proyectos medianos. En desarrollo no compila todo el proyecto, sino que sirve los módulos bajo demanda y aplica Hot Module Replacement, de modo que al guardar un fichero CSS o JavaScript la página se actualiza sin recargarla entera. En producción, cada fichero recibe un hash basado en su contenido, lo que permite caché agresiva en el navegador sin riesgo de servir versiones obsoletas.
Configuración base: vite.config.js y laravel-vite-plugin
Un proyecto Laravel 13 recién creado ya incluye la configuración lista para usar. El fichero vite.config.js define el plugin con las entradas por defecto y, con eso, los comandos npm run dev y npm run build funcionan sin tocar nada más.
Las entradas por defecto: resources/css/app.css y resources/js/app.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
Las entradas son los puntos de partida que Vite procesa: todo lo que importes desde app.css o app.js se agrupa, se resuelve y acaba en el build final. Si añades más ficheros de entrada, solo tienes que incluirlos en el array input.
La directiva @vite en Blade
En tu layout, una sola línea carga todo lo necesario:
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
La directiva @vite detecta automáticamente si el servidor de desarrollo de Vite está corriendo: si lo está, inyecta el cliente de Vite y habilita el HMR; si no, resuelve los ficheros compilados desde el manifest y genera las etiquetas link y script correspondientes, incluido el CSS que se importa desde JavaScript.
Desarrollo con HMR
npm run dev y el fichero public/hot
Al ejecutar npm run dev, Vite arranca su servidor de desarrollo y crea un fichero public/hot que contiene la URL del servidor. Cuando Laravel detecta ese fichero, sabe que debe servir los assets desde el dev server en lugar de desde el disco. Si el fichero hot queda huérfano tras un despliegue, el navegador intentará cargar los assets desde una dirección inexistente: el clásico síntoma de una página sin estilos en producción.
HMR en dominios personalizados: server.host y server.hmr.host
Por defecto, Vite escucha en localhost y el cliente de HMR apunta ahí. Si tu aplicación se sirve en un dominio de desarrollo como blenderdeluxe.test, los assets fallarán porque el navegador pedirá recursos a localhost. La solución es declarar el host en la configuración:
server: {
host: 'blenderdeluxe.test',
hmr: {
host: 'blenderdeluxe.test',
},
},
Vite en Docker y entornos remotos: publicDirectory
Dentro de un contenedor, Vite debe escuchar en una interfaz accesible desde el host y el plugin necesita saber dónde está la carpeta pública de la aplicación. Con el flag --host y la opción publicDirectory del plugin, el dev server responde correctamente aunque la aplicación viva en /var/www/html:
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
publicDirectory: '/var/www/html/public',
})
Producción: npm run build y el manifest
public/build/manifest.json y los hashes de contenido
npm run build genera la carpeta public/build con los ficheros optimizados y un manifest.json que mapea cada nombre original con su versión con hash, por ejemplo app.js convertido en app-3f8c1d2a.js. La directiva @vite lee ese manifest en tiempo de ejecución, así que nunca tienes que escribir los nombres con hash a mano: cambian en cada build y el backend los resuelve solo.
Desplegar con CI y servir desde CDN
Lo habitual es ejecutar npm ci y npm run build dentro del pipeline de integración continua y subir la carpeta public/build junto con el resto del despliegue. Como los nombres incluyen el hash del contenido, puedes servir los assets desde un CDN con cabeceras de caché muy largas: cuando el contenido cambia, el nombre cambia y el navegador pide el fichero nuevo.
Fuentes autohospedadas en Laravel 13
El plugin resuelve, emite y genera el CSS de las fuentes
Depender de Google Fonts significa ceder peticiones a un tercero en cada visita: otra conexión, otro DNS y una dependencia de disponibilidad que no controlas. En Laravel 13, el plugin de Vite puede encargarse de las fuentes: importas la fuente en tu CSS, el plugin resuelve los ficheros, los emite como assets de Vite con su hash, genera el CSS @font-face correspondiente y escribe un font manifest con los ficheros finales.
La directiva @fonts y el font manifest
Para cargar esas fuentes en la página, Blade dispone de la directiva @fonts, que lee el font manifest y genera los preloads y las declaraciones necesarias:
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fonts
</head>
El resultado es que el navegador sirve las fuentes desde tu propio dominio, con el mismo hashing y la misma estrategia de caché que el resto de assets, sin ninguna petición a terceros.
Varias entradas, lazy loading y code splitting
@vite con arrays de entrada
Si tu aplicación tiene una zona de administración con sus propios estilos, añade la entrada al plugin y pásala a la directiva:
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/admin.js'])
Importaciones dinámicas para recortar el JavaScript inicial
Para páginas donde un componente pesa más de lo que merece la pena cargar al inicio, las importaciones dinámicas dividen el bundle: el código se descarga solo cuando se necesita, y Vite genera los chunks correspondientes en el build.
const chart = await import('./charts');
chart.render(document.getElementById('stats'));
Problemas frecuentes y soluciones
@vite no carga nada: plugin ausente, hot file y .mjs
Cuando la página aparece sin estilos ni JavaScript, los culpables habituales son tres: laravel-vite-plugin no está instalado o no está declarado en vite.config.js, quedó un fichero public/hot residual que apunta a un dev server que ya no existe, o el fichero de configuración se llama vite.config.mjs y el plugin no se está cargando. Revisa esos tres puntos en ese orden y el problema se resuelve solo.
Minificación de CSS con Lightning CSS
Vite minifica el CSS con Lightning CSS por defecto, un compilador rápido escrito en Rust que además puede transformar propiedades modernas a sintaxis compatible con navegadores antiguos. Si algún día necesitas volver al minificador clásico de esbuild, la opción build.cssMinify lo permite sin tocar el resto del pipeline.
Ejemplo completo: Tailwind + Alpine servidos con Vite en Laravel 13
Este mismo blog es el ejemplo: Tailwind CSS para los estilos y Alpine.js para la interactividad, ambos servidos con Vite en Laravel 13. El punto de entrada app.js importa el CSS de Tailwind, arranca Alpine y deja todo listo para usar directivas como x-data en las plantillas Blade:
import '../css/app.css';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
En desarrollo, npm run dev mantiene el HMR activo y los cambios en los ficheros de Tailwind se reflejan al instante. En producción, npm run build genera los ficheros con hash y el manifest; la fuente del blog se sirve autohospedada con @fonts y el despliegue sube public/build sin más ceremonia.
Conclusión
Entender qué ocurre entre npm run dev y npm run build convierte a Vite en una herramienta predecible: el fichero hot en desarrollo, el manifest con hashes en producción, el host del HMR en dominios propios y las fuentes autohospedadas con @fonts en Laravel 13. Con este pipeline dominado, los assets dejan de ser una caja negra y pasan a ser una parte más del despliegue. Si quieres seguir afinando tu stack de Laravel 13, en este blog ya tienes guías de Tailwind, Alpine y el rendimiento con caché.