wandres.dev
CONFIGURAR VITE · plugins y resolve

css y build: preprocesadores, target, outDir y salida

Las dos claves que convierten tu código en bytes servibles: css con preprocesadores, modules y el motor Lightning CSS, y build con target, outDir, minify y las opciones de salida que de verdad se tocan en Vite 8.

⏱ 18 min

Las claves css y build son donde la verdad de desarrollo se convierte en los bytes que sirves. css gobierna cómo Vite entiende tus estilos —preprocesadores como Sass, módulos con nombres locales, y en 2026 el motor Lightning CSS escrito en Rust— y build decide la forma del artefacto final: para qué navegadores compilas, dónde cae, cómo se parte y cuánto se minifica. Sobre Vite 8 con Rolldown, muchas de estas opciones estrenan motor pero conservan su nombre. Esta lección cierra el nivel con las palancas que realmente se ajustan y el criterio para no tocar las que no debes.

🎯 Al terminar esta lección sabrás
  • Configurar preprocesadores y CSS Modules bajo la clave css.
  • Conocer Lightning CSS como transformador y minificador moderno.
  • Fijar build.target entendiendo su impacto en compatibilidad y peso.
  • Ajustar outDir, minify y la partición de la salida con criterio.

css: preprocesadores, modules y el motor

Vite entiende CSS de forma nativa, pero la clave css deja afinar tres cosas. preprocessorOptions pasa opciones a Sass, Less o Stylus: el uso más común es additionalData, que inyecta una cabecera —variables o mixins compartidos— en cada archivo sin que tengas que importarla a mano. Con Sass moderno conviene fijar la API nueva para evitar los avisos de deprecación del compilador antiguo.

import { defineConfig } from "vite"

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/vars.scss" as *;`,
        api: "modern-compiler",
      },
    },
    modules: {
      localsConvention: "camelCaseOnly",
      generateScopedName: "[name]__[local]___[hash:base64:5]",
    },
    devSourcemap: true,
  },
})

modules gobierna los CSS Modules, el mecanismo que aísla los nombres de clase por archivo para que dos componentes puedan usar .title sin pisarse. localsConvention decide cómo llegan esos nombres a tu JavaScript —camelCaseOnly convierte mi-clase en miClase— y generateScopedName controla el patrón del nombre único generado, útil para hacer legible el CSS en desarrollo y compacto en producción. devSourcemap te da mapas de origen del CSS en dev, para que el inspector señale tu archivo fuente y no el transformado.

El motor que procesa todo esto está cambiando. Durante años fue PostCSS; en 2026, con Vite volcado a herramientas en Rust, Lightning CSS gana terreno como transformador y minificador: hace el prefijado de vendedor, el downleveling de sintaxis moderna y la minificación en una sola pasada nativa, mucho más rápida. Se activa eligiéndolo como transformador, y entonces sustituye a la cadena de PostCSS.

export default defineConfig({
  css: {
    transformer: "lightningcss",
  },
  build: {
    cssMinify: "lightningcss",
  },
})
ℹ️
Lightning CSS no ejecuta la cadena de PostCSS

Al elegir lightningcss como transformador, tu configuración de PostCSS y los preprocesadores dejan de aplicarse por esa vía: Lightning CSS no es un plugin de PostCSS, es un motor alternativo completo. Es una decisión de arquitectura, no un ajuste incremental. Gana velocidad y prefijado moderno, pero si dependes de plugins de PostCSS concretos tendrás que comprobar que Lightning CSS cubre lo mismo antes de migrar. Como todo en este nivel: entiende qué motor corre de verdad antes de confiar en él.

build: target, outDir y la forma de la salida

build describe el artefacto. La opción más consecuente es target: el nivel de sintaxis al que Vite rebaja tu código. Desde Vite 7 el valor por defecto es baseline-widely-available, que apunta a las capacidades soportadas de forma amplia y estable por los navegadores, un objetivo móvil que se actualiza con el consenso de la plataforma en lugar de fijar versiones a mano. Bajarlo compila para navegadores más antiguos a costa de más código y menos velocidad; subirlo produce bundles más pequeños que exigen navegadores recientes.

export default defineConfig({
  build: {
    target: "baseline-widely-available",
    outDir: "dist",
    assetsDir: "assets",
    sourcemap: true,
    cssCodeSplit: true,
    minify: "esbuild",
    emptyOutDir: true,
    chunkSizeWarningLimit: 600,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
        },
      },
    },
  },
})

outDir es la carpeta de salida —dist por defecto— y assetsDir la subcarpeta de recursos con hash dentro de ella. emptyOutDir limpia esa carpeta antes de cada build para que no queden restos de una compilación previa. sourcemap decide si emites mapas de origen para depurar producción, y cssCodeSplit mantiene el CSS partido por chunk en vez de un único archivo monolítico, de modo que cada ruta cargue solo su estilo.

flowchart LR
A[codigo fuente] --> B[transform de css y ts]
B --> C[rolldown empaqueta el grafo]
C --> D[minify y downlevel al target]
D --> E[archivos con hash en outDir]
E --> F[index.html apunta a los assets]
style D fill:#a6e3a1,color:#11111b

Las opciones que de verdad se tocan

En el día a día, tres palancas concentran casi todos los ajustes. minify elige minificador: el built-in rápido por defecto, o terser cuando necesitas su compresión más agresiva en casos límite, asumiendo que es más lento. manualChunks, dentro de rollupOptions, separa dependencias estables —tu framework— en un chunk propio que el navegador cachea entre despliegues, en vez de reempaquetarlo con tu código que cambia a diario. Y chunkSizeWarningLimit calla o endurece el aviso de chunk grande según lo que tu proyecto considere razonable.

🎯

target

A qué navegadores compilas. baseline-widely-available es un objetivo vivo; bajarlo pesa más, subirlo exige navegadores recientes.

📦

outDir y emptyOutDir

Dónde cae el build y si se limpia antes. Evita servir restos de una compilación vieja.

🗜️

minify

El minificador rápido por defecto, o terser para exprimir bytes en casos límite a costa de tiempo.

🧩

manualChunks

Aísla dependencias estables para que el navegador las cachee entre despliegues.

Dos opciones más aparecen en cuanto integras el build con un backend. manifest emite un manifest.json que mapea cada entrada a su archivo con hash, para que un servidor —Rails, Django, Laravel— sepa qué script y qué link inyectar en su plantilla en lugar de adivinar nombres con hash. modulePreload gobierna las etiquetas de precarga que Vite añade para adelantar la descarga de los chunks que sabes que harán falta, recortando cascadas de red en la primera carga.

build: {
  manifest: true,
  assetsInlineLimit: 4096,
  reportCompressedSize: false,
}

assetsInlineLimit decide el umbral por debajo del cual Vite incrusta un recurso como data URI en lugar de emitir un archivo aparte: ahorra una petición a costa de algo de peso en el documento. sourcemap, por su parte, merece una decisión consciente en producción: emitirlos y servirlos expone tu código fuente a cualquiera, y no emitirlos deja tus reportes de error como pilas de líneas minificadas ilegibles. El punto intermedio habitual es generarlos pero no publicarlos, subiéndolos solo a tu plataforma de seguimiento de errores, que los usa para desminificar los stack traces sin que el mapa llegue al navegador de nadie.

📝
emptyOutDir fuera de la raíz pide confirmación

emptyOutDir borra la carpeta de salida antes de cada build, y eso es inofensivo mientras outDir viva dentro del proyecto. Pero si apuntas outDir a una carpeta fuera de la raíz —el public de un backend, por ejemplo— Vite te pide confirmación antes de vaciarla, porque un borrado automático de una ruta ajena al proyecto es exactamente la clase de operación que arrasa trabajo que no era tuyo. Cuando veas ese aviso, léelo: es una red, no una molestia.

ℹ️
rollupOptions conserva el nombre aunque el motor sea Rolldown

En Vite 8 el bundler de producción es Rolldown, escrito en Rust, no Rollup. Pero la clave sigue llamándose rollupOptions por compatibilidad, y Vite mapea esas opciones sobre el nuevo motor. Casos habituales como output.manualChunks funcionan igual; algunas opciones muy específicas de Rollup pueden tener equivalentes propios en Rolldown. Si publicas una librería en vez de una app, mira además build.lib, que ata este build al empaquetado de paquetes del nivel de publicación.

target es la perilla donde eliges a quién sirves y cuánto pesa

De todas las opciones de build, target es la que más consecuencias tiene y la que más gente ajusta sin entender, porque codifica una tensión que no tiene solución perfecta, solo un punto elegido: cada navegador antiguo que decides soportar te cuesta bytes que pagan todos tus usuarios, incluidos los que llevan el navegador al día. Compilar para atrás no es gratis ni neutro: el downleveling de sintaxis moderna a una vieja añade polyfills y transformaciones que inflan el bundle y lo hacen algo más lento de ejecutar, y ese coste lo carga cada visita, no solo la del navegador rezagado. Por eso el giro de Vite hacia baseline-widely-available es tan profundo: en vez de que fijes una versión concreta y la olvides —congelando tu build en el pasado del día que la escribiste—, apunta a un objetivo que se mueve con el consenso de la plataforma, de modo que a medida que las capacidades modernas se vuelven universales, tu bundle adelgaza solo, sin que toques nada. La decisión de target es, en el fondo, una decisión de producto disfrazada de opción técnica: quién es tu usuario, con qué navegador llega, y cuánto peso estás dispuesto a imponerle a la mayoría para no dejar fuera a la minoría. No hay respuesta universal, pero sí hay una disciplina: mide tu audiencia real en vez de suponerla, elige el target más alto que esa audiencia tolere, y revísalo con el tiempo porque el suelo sube. Un build no es solo código que funciona; es un contrato sobre a quién sirves y a qué precio, y target es la cláusula donde ese contrato se firma.

⚔️ Afina tu salida de producción
  1. Configura un preprocesador con additionalData para inyectar variables Sass compartidas y verifica que un componente las ve sin importarlas.
  2. Activa CSS Modules en un archivo y comprueba en el build que las clases salen con nombre único según tu generateScopedName.
  3. Cambia build.target a un valor más bajo, mide el tamaño del bundle, y vuelve a baseline-widely-available para comparar.
  4. Separa tu framework en un chunk propio con manualChunks y confirma que su hash no cambia al editar tu código de aplicación.
  5. Prueba terser frente al minificador por defecto y decide si la diferencia de bytes justifica la de tiempo en tu proyecto.