Frontend

Vite

Referência completa de Vite — configuração, plugins, variáveis de ambiente, assets, SSR, Environment API, Vitest e deploy

Como o Vite funciona — ESM nativo, esbuild, Rollup

Vite separa o processo em dois modos distintos com ferramentas diferentes para cada um.

Desenvolvimento: o servidor dev serve arquivos via HTTP usando ESM nativo do browser. Cada arquivo é transformado sob demanda quando requisitado — sem bundle. O browser resolve os imports diretamente. Isso torna o start quase instantâneo independente do tamanho do projeto. esbuild (escrito em Go) faz o pre-bundling das dependências de node_modules — converte CommonJS para ESM e agrupa dependências com muitos arquivos internos (ex: lodash com 600 módulos vira um único arquivo). HMR (Hot Module Replacement) atualiza apenas o módulo que mudou, sem recarregar a página.

Produção: usa Rollup para gerar bundles otimizados com tree-shaking, code splitting e hashing de assets. O Vite 5+ usa o Rollup 4 (partes em Rust). O Vite 7 migra para Rolldown (Rollup reescrito em Rust) internamente, com build 10-20× mais rápido.

Desenvolvimento:
Browser → Vite Dev Server (ESM nativo) → Transforma sob demanda

              esbuild pre-bundle deps (node_modules)

Produção:
Vite build → Rollup/Rolldown → bundle otimizado → dist/

Setup inicial — scaffolding, estrutura de projeto

O create-vite é o scaffolding oficial. Suporta vanilla, Vue, React, Preact, Lit, Svelte, Solid, Qwik — com variantes TypeScript.

# Interativo
npm create vite@latest meu-projeto

# Direto com template
npm create vite@latest meu-projeto -- --template vue-ts
npm create vite@latest meu-projeto -- --template react-ts
npm create vite@latest meu-projeto -- --template vanilla-ts

# Templates disponíveis
# vanilla, vanilla-ts
# vue, vue-ts
# react, react-ts, react-swc, react-swc-ts
# preact, preact-ts
# lit, lit-ts
# svelte, svelte-ts
# solid, solid-ts
# qwik, qwik-ts

cd meu-projeto
npm install
npm run dev

Estrutura recomendada para projeto Vue/React:

projeto/
├── public/                  # assets estáticos — copiados sem processamento
│   ├── favicon.ico
│   └── robots.txt
├── src/
│   ├── main.ts              # entry point — monta o app
│   ├── App.vue
│   ├── assets/              # assets processados pelo Vite (import explícito)
│   │   ├── images/
│   │   └── fonts/
│   ├── components/
│   ├── composables/         # Vue composables / React hooks
│   ├── pages/
│   ├── stores/              # Pinia / Zustand
│   ├── styles/
│   │   └── global.css
│   ├── types/
│   └── utils/
├── index.html               # entry HTML — na raiz (não em /public)
├── vite.config.ts
├── tsconfig.json
├── .env
├── .env.development
├── .env.production
└── .env.local               # nunca commitar

O index.html fica na raiz do projeto (não dentro de src/ ou public/) porque o Vite o trata como entry point e o serve diretamente. Ele entende <script type="module" src="/src/main.ts"> nativamente.


vite.config.ts completo

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'

export default defineConfig(({ command, mode }) => {
  // Carrega variáveis de ambiente para usar na config (não apenas no app)
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [
      vue(),
    ],

    resolve: {
      alias: {
        '@': resolve(import.meta.dirname, 'src'),
        '@components': resolve(import.meta.dirname, 'src/components'),
        '@utils': resolve(import.meta.dirname, 'src/utils'),
      },
      // Extensões tentadas por ordem (raramente precisa mudar)
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
    },

    server: {
      port: 5173,
      host: true,         // expõe para a rede local (0.0.0.0)
      open: true,          // abre o browser ao iniciar
      strictPort: false,   // tenta próxima porta se ocupada
      cors: true,          // habilita CORS no dev server
      https: false,        // ou objeto com cert/key para HTTPS

      proxy: {
        '/api': {
          target: env.VITE_API_URL || 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
        '/ws': {
          target: 'ws://localhost:8080',
          ws: true,
        },
      },

      // HMR — Hot Module Replacement
      hmr: {
        overlay: true,    // mostra overlay de erro no browser
      },
    },

    preview: {
      port: 4173,
      open: true,
    },

    build: {
      outDir: 'dist',
      assetsDir: 'assets',  // subdir dentro de outDir para assets
      sourcemap: true,       // 'inline' | 'hidden' | true | false
      minify: 'esbuild',     // 'esbuild' (padrão) | 'terser' | false
      target: 'es2022',      // target ECMAScript mínimo
      chunkSizeWarningLimit: 500, // kB — avisa chunks maiores que isso

      rollupOptions: {
        output: {
          manualChunks: {
            'vendor-vue': ['vue', 'vue-router', 'pinia'],
            'vendor-ui':  ['@headlessui/vue'],
          },
          // Naming das saídas
          chunkFileNames: 'assets/[name]-[hash].js',
          entryFileNames: 'assets/[name]-[hash].js',
          assetFileNames: 'assets/[name]-[hash][extname]',
        },
      },

      // Cópia de assets grandes sem processar
      copyPublicDir: true,
    },

    css: {
      modules: {
        // Padrão de nome de classe em CSS Modules
        localsConvention: 'camelCaseOnly',
        generateScopedName: '[name]__[local]___[hash:base64:5]',
      },
      preprocessorOptions: {
        scss: {
          // Injeta variáveis globais em todo arquivo .scss
          additionalData: `@use "@/styles/variables" as *;`,
        },
        less: {
          math: 'parens-division',
        },
      },
    },

    // Constantes injetadas no código em tempo de build (como define do Webpack)
    define: {
      __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
      __BUILD_DATE__: JSON.stringify(new Date().toISOString()),
    },
  }
})

Variáveis de ambiente — .env, VITE_ prefix, import.meta.env, tipagem, modo customizado

Vite usa o padrão dotenv para carregar variáveis. Apenas variáveis com prefixo VITE_ são expostas no código do cliente — as demais ficam apenas no processo Node (disponíveis em vite.config.ts via loadEnv).

# .env — base, todos os ambientes
VITE_API_URL=http://localhost:8080
VITE_APP_NAME="Meu Sistema"
DATABASE_URL=postgres://...   # sem VITE_ — apenas no Node/config

# .env.development — sobrescreve em modo dev
VITE_API_URL=http://localhost:3000
VITE_DEBUG=true

# .env.production — sobrescreve no build (npm run build)
VITE_API_URL=https://api.meusite.com

# .env.staging — modo customizado
VITE_API_URL=https://staging-api.meusite.com

# .env.local — sobrescreve qualquer um — NUNCA commitar
VITE_SECRET=chave-local-de-dev
STRIPE_TEST_KEY=sk_test_...

# .env.development.local — local + development
// Acesso no código do app
const apiUrl   = import.meta.env.VITE_API_URL
const appName  = import.meta.env.VITE_APP_NAME

// Variáveis embutidas pelo Vite (sempre disponíveis)
import.meta.env.MODE      // 'development' | 'production' | 'staging' | 'test'
import.meta.env.DEV       // true em desenvolvimento
import.meta.env.PROD      // true em produção
import.meta.env.SSR       // true em contexto SSR
import.meta.env.BASE_URL  // base configurada em vite.config.ts

Tipagem com TypeScript:

// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_NAME: string
  readonly VITE_DEBUG?: string
  readonly VITE_STRIPE_PUBLIC_KEY: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Modo customizado:

# Build para staging
npx vite build --mode staging

# Dev com arquivo .env.test
npx vite --mode test
// vite.config.ts — carregar variáveis de ambiente na config
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  // '' como terceiro arg = sem filtro de prefixo (inclui DATABASE_URL etc)
  const env = loadEnv(mode, process.cwd(), '')

  return {
    define: {
      'process.env.DATABASE_URL': JSON.stringify(env.DATABASE_URL),
    },
  }
})

CSS — modules, preprocessors, PostCSS, inline CSS

<!-- CSS Modules em Vue SFC -->
<style module>
.button {
  background: var(--color-primary);
  padding: 0.5rem 1rem;
}
.active {
  background: var(--color-active);
}
</style>

<script setup>
// Acessa as classes como objeto
import { useCssModule } from 'vue'
const css = useCssModule()
// Ou via $style no template
</script>

<template>
  <button :class="[$style.button, { [$style.active]: isActive }]">
    Clique
  </button>
</template>
<!-- Sass em SFC Vue -->
<style lang="scss">
@use '@/styles/mixins' as *;

.card {
  @include flex-center;
  padding: $spacing-md;

  &:hover {
    transform: translateY(-2px);
  }

  // v-bind CSS reativo — variável CSS gerada pelo Vue
  color: v-bind(textColor);
}
</style>
// vite.config.ts — PostCSS embutido
import autoprefixer from 'autoprefixer'
import tailwindcss from 'tailwindcss'

export default defineConfig({
  css: {
    postcss: {
      plugins: [
        tailwindcss(),
        autoprefixer(),
      ],
    },
    // Ou referencia postcss.config.js na raiz
  },
})

Assets — importação, public vs src/assets, ?url, ?raw, ?worker

public/ — arquivos copiados sem processamento para a raiz do dist/. Acesse via URL absoluta (/favicon.ico). Não fazem parte do grafo de módulos — sem hashing, sem import explícito necessário.

src/assets/ — processados pelo Vite. Recebem hash no nome (cache busting). Devem ser importados explicitamente no código.

// Import de imagem — retorna URL do asset em runtime
import logoUrl from '@/assets/logo.png'
// Em Vue: <img :src="logoUrl" />

// ?url — força retornar URL (útil para tipos ambíguos)
import svgUrl from '@/assets/icon.svg?url'

// ?raw — retorna o conteúdo como string
import svgContent from '@/assets/icon.svg?raw'
// Em Vue: <div v-html="svgContent"></div>

// ?inline — força inline como base64 (independente do tamanho)
import tinyPng from '@/assets/tiny.png?inline'

// Fontes — importar no CSS ou no main.ts
import '@/assets/fonts/inter.css'
<!-- URL dinâmica de asset — Vite precisa ver o caminho em build time -->
<!-- NÃO funciona: :src="`/assets/${name}.png`" com string interpolada -->
<!-- Use: import estático ou new URL() -->

<script setup>
// new URL() — funciona com paths dinâmicos parcialmente conhecidos
function getAssetUrl(name: string) {
  return new URL(`../assets/icons/${name}.svg`, import.meta.url).href
}
</script>

Glob imports — import.meta.glob

import.meta.glob importa múltiplos arquivos via padrão glob. Implementado pelo Vite como transformação em build time — não é API do browser.

// Lazy (padrão) — cada arquivo vira um import() dinâmico
const modules = import.meta.glob('./pages/*.vue')
// Resultado:
// { './pages/Home.vue': () => import('./pages/Home.vue'), ... }

// Eager — importa tudo imediatamente (síncrono)
const eagerModules = import.meta.glob('./pages/*.vue', { eager: true })
// Resultado:
// { './pages/Home.vue': { default: HomeComponent, ... }, ... }

// Múltiplos padrões
const mixed = import.meta.glob(['./components/**/*.vue', '!**/*.test.vue'])

// Importar apenas o default export
const pages = import.meta.glob('./pages/*.vue', {
  eager: true,
  import: 'default', // apenas o export default
})

// Importar export nomeado
const routes = import.meta.glob('./routes/*.ts', {
  eager: true,
  import: 'route',
})

// Exemplo: registro automático de rotas
const pageFiles = import.meta.glob('./pages/**/*.vue', { eager: true })
const autoRoutes = Object.entries(pageFiles).map(([path, module]) => ({
  path: path
    .replace('./pages', '')
    .replace('.vue', '')
    .replace('/index', '/'),
  component: (module as { default: Component }).default,
}))

// Exemplo: i18n — carrega mensagens de todos os locales
const localeFiles = import.meta.glob('./locales/*.json', { eager: true })
const messages = Object.fromEntries(
  Object.entries(localeFiles).map(([path, mod]) => [
    path.match(/\/([^/]+)\.json$/)?.[1],
    (mod as { default: Record<string, string> }).default,
  ])
)

JSON imports e dados estáticos

// Import direto — gera objeto JavaScript
import config from './config.json'
console.log(config.apiUrl)

// Import named — apenas a propriedade desejada (tree-shaking)
import { version } from '../package.json'

// ?url — retorna a URL do arquivo JSON (útil para fetch em runtime)
import configUrl from './big-data.json?url'
const data = await fetch(configUrl).then(r => r.json())

// ?raw — retorna como string (útil para exibir JSON, validar schema)
import schemaStr from './schema.json?raw'
const schema = JSON.parse(schemaStr)

WebWorkers — ?worker, inline, shared workers

// Worker em arquivo separado — cria um novo bundle
import MyWorker from './workers/heavy.worker.ts?worker'
const worker = new MyWorker()

worker.postMessage({ data: bigArray })
worker.onmessage = (e) => {
  console.log('Resultado:', e.data)
}

// Inline worker — inlined como base64 no bundle principal
import InlineWorker from './workers/small.worker.ts?worker&inline'
const inline = new InlineWorker()

// ?url — importa apenas a URL (instancia manualmente com new Worker)
import workerUrl from './workers/custom.worker.ts?worker&url'
const manual = new Worker(workerUrl, { type: 'module' })

// Shared worker
import SharedWorker from './workers/shared.worker.ts?sharedworker'
const shared = new SharedWorker()
shared.port.start()
shared.port.postMessage('hello')

// workers/heavy.worker.ts — estrutura de um worker
self.onmessage = async (e: MessageEvent) => {
  const { data } = e.data
  const result = await heavyComputation(data)
  self.postMessage(result)
}

WebAssembly — import de .wasm

// ?init — instancia com WebAssembly.instantiate
import init from './lib/encoder.wasm?init'

const instance = await init()
const encoded = instance.exports.encode(inputBuffer)

// Com importObject — passa funções JavaScript para o WASM
import init from './lib/crypto.wasm?init'

const instance = await init({
  imports: {
    log: (ptr: number, len: number) => {
      console.log('WASM log:', readString(ptr, len))
    },
  },
})

// ?url — URL do arquivo (para WebAssembly.instantiateStreaming manual)
import wasmUrl from './lib/heavy.wasm?url'
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl))

Proxy de API — server.proxy, rewrite, CORS em dev

O proxy do dev server evita problemas de CORS durante o desenvolvimento. As requests do browser vão para o Vite, que as repassa para o backend.

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      // String shorthand — redireciona /foo/* para http://localhost:4567/foo/*
      '/foo': 'http://localhost:4567',

      // Com opções completas
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true, // muda o header Host para o target
        rewrite: (path) => path.replace(/^\/api/, ''), // /api/users → /users
        // bypass — função para lógica customizada
        bypass(req, res, options) {
          if (req.headers.accept?.includes('text/html')) {
            return '/index.html' // não faz proxy, serve index.html
          }
        },
      },

      // RegExp — captura qualquer /v1/ ou /v2/
      '^/(v1|v2)/.*': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },

      // WebSocket
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true,
        rewriteWsOrigin: true, // cuidado: CSRF
      },

      // Múltiplos backends com base no path
      '/auth': {
        target: 'http://keycloak:8180',
        changeOrigin: true,
      },
      '/files': {
        target: 'http://minio:9000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/files/, ''),
      },
    },
  },
})

HMR API — import.meta.hot

A HMR API permite que módulos se registrem para receber atualizações sem reload da página. Vite injeta import.meta.hot apenas em desenvolvimento.

// Aceitar atualizações do próprio módulo
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // newModule é o módulo atualizado
    // Se undefined, módulo foi invalidado
    if (newModule) {
      console.log('Módulo atualizado:', newModule)
    }
  })
}

// Aceitar atualizações de dependências específicas
if (import.meta.hot) {
  import.meta.hot.accept(['./dep1', './dep2'], ([newDep1, newDep2]) => {
    // Executado quando qualquer uma das deps muda
  })
}

// dispose — limpar side effects antes de aplicar hot update
if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // data é persistido entre hot updates
    clearInterval(data.interval)
    socket?.disconnect()
  })

  import.meta.hot.data.interval = setInterval(pollServer, 5000)
}

// invalidate — forçar reload completo do módulo
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    if (needsFullReload) {
      import.meta.hot!.invalidate('Mudança requer reload completo')
    }
  })
}

// on/off — ouvir eventos do HMR
if (import.meta.hot) {
  import.meta.hot.on('vite:beforeUpdate', (payload) => {
    console.log('HMR update incoming:', payload)
  })
}

Plugins essenciais

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import react from '@vitejs/plugin-react-swc'
import { VitePWA } from 'vite-plugin-pwa'
import tsconfigPaths from 'vite-tsconfig-paths'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    // Vue SFC support
    vue({
      script: { defineModel: true }, // garante defineModel em Vue < 3.4 (já padrão no 3.4+)
    }),

    // JSX em Vue
    vueJsx(),

    // React com SWC (mais rápido que Babel)
    react(),

    // PWA — service worker, manifest, precaching automático
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'Meu App',
        short_name: 'App',
        theme_color: '#3b82f6',
        icons: [
          { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
      },
    }),

    // tsconfigPaths — usa os paths do tsconfig.json automaticamente
    // alternativa ao alias manual no vite.config.ts
    tsconfigPaths(),

    // unplugin-auto-import — importa Vue, Router, Pinia etc automaticamente
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
      dts: 'src/auto-imports.d.ts',
      eslintrc: { enabled: true }, // gera .eslintrc-auto-import.json
    }),

    // unplugin-vue-components — importa componentes automaticamente
    Components({
      dts: 'src/components.d.ts',
      // Suporte a bibliotecas UI (Naive UI, Ant Design Vue, Element Plus)
      resolvers: [/* NaiveUiResolver() */],
    }),

    // Visualizer — mapa visual do bundle (usar apenas em analysis)
    process.env.ANALYZE && visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html',
    }),
  ].filter(Boolean),
})

Build options — rollupOptions, manualChunks, target, minify, sourcemap

export default defineConfig({
  build: {
    // Target — define a sintaxe JS mínima do output
    // 'es2015', 'es2020', 'es2022', 'esnext'
    // 'modules' — browsers que suportam ESM nativo
    // Array: ['chrome80', 'firefox78', 'safari13', 'edge79']
    target: 'es2022',

    // Minificação
    minify: 'esbuild',   // padrão — mais rápido
    // minify: 'terser', // mais agressivo, mais lento; requer terser instalado
    // minify: false,    // útil para debug

    terserOptions: {
      compress: {
        drop_console: true,  // remove console.log em produção
        drop_debugger: true,
      },
    },

    // Source maps
    sourcemap: true,          // gera .map separado
    // sourcemap: 'inline',   // embutido no bundle
    // sourcemap: 'hidden',   // gera .map mas não referencia no bundle (Sentry)

    // Inlining de assets pequenos como base64
    assetsInlineLimit: 4096,  // 4KB padrão; 0 para desabilitar

    rollupOptions: {
      input: {
        main:  'index.html',
        admin: 'admin.html', // multi-page app
      },

      output: {
        // Code splitting manual — separa vendors de código próprio
        manualChunks(id) {
          // Função mais flexível que o objeto
          if (id.includes('node_modules')) {
            if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
              return 'vendor-vue'
            }
            if (id.includes('chart.js')) {
              return 'vendor-charts'
            }
            return 'vendor' // demais deps em um único chunk
          }
        },

        // Nomenclatura dos arquivos gerados
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: ({ name }) => {
          if (/\.(png|jpe?g|svg|gif|webp)$/.test(name ?? '')) return 'images/[name]-[hash][extname]'
          if (/\.(woff2?|eot|ttf|otf)$/.test(name ?? '')) return 'fonts/[name]-[hash][extname]'
          if (/\.css$/.test(name ?? '')) return 'css/[name]-[hash][extname]'
          return 'assets/[name]-[hash][extname]'
        },
      },

      // Externalizar libs (não incluir no bundle)
      // útil em library mode ou quando disponível globalmente (CDN)
      external: ['vue'],
    },

    // Avisa sobre chunks maiores que X kB (padrão 500)
    chunkSizeWarningLimit: 1000,

    // CSS code splitting — separa CSS de cada chunk
    cssCodeSplit: true,

    // Não esvaziar o outDir antes de cada build (útil em multi-build)
    emptyOutDir: true,
  },
})

Library mode — publicar pacote npm

// vite.config.ts para publicar como biblioteca
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'
import dts from 'vite-plugin-dts' // gera arquivos .d.ts

export default defineConfig({
  plugins: [
    vue(),
    dts({
      rollupTypes: true, // empacota todas as declarações em um único .d.ts
    }),
  ],

  build: {
    lib: {
      entry: resolve(import.meta.dirname, 'src/index.ts'),
      name: 'MeuComponente',            // nome global para UMD/IIFE
      formats: ['es', 'cjs'],           // ESM + CommonJS
      // formats: ['es', 'cjs', 'umd'],  // + UMD para browsers sem bundler
      fileName: (format) => `index.${format}.js`,
    },

    rollupOptions: {
      // Externalizar Vue — quem instalar a lib já tem Vue
      external: ['vue', 'vue-router'],
      output: {
        globals: {
          vue: 'Vue',
          'vue-router': 'VueRouter',
        },
        // Preservar módulos — útil para tree-shaking pelo consumidor
        preserveModules: true,
        preserveModulesRoot: 'src',
        exports: 'named',
      },
    },
  },
})
// package.json — campos para publicação de lib
{
  "name": "meu-componente-vue",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs.js",
  "module": "./dist/index.es.js",
  "exports": {
    ".": {
      "import": "./dist/index.es.js",
      "require": "./dist/index.cjs.js"
    }
  },
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.0.0"
  }
}

SSR mode — build.ssr, entry server, manifestação

O modo SSR do Vite gera dois bundles: client (para o browser) e server (para Node.js).

// vite.config.ts
export default defineConfig({
  build: {
    // Client build (normal)
    // npx vite build -- entry point padrão é index.html

    // Server build
    // npx vite build --ssr src/entry-server.ts
    ssr: 'src/entry-server.ts', // ou true para usar ssrManifest
    outDir: 'dist/server',

    rollupOptions: {
      input: 'src/entry-server.ts',
    },
  },
})
// src/entry-server.ts — render function para SSR
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'
import { createRouter } from './router'
import { createPinia } from 'pinia'

export async function render(url: string) {
  const app    = createSSRApp(App)
  const router = createRouter()
  const pinia  = createPinia()

  app.use(router).use(pinia)
  await router.push(url)
  await router.isReady()

  const html = await renderToString(app)
  const state = JSON.stringify(pinia.state.value)

  return { html, state }
}
// src/entry-client.ts — hydration no browser
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createPinia } from 'pinia'

const app    = createApp(App)
const pinia  = createPinia()
const router = createRouter()

// Restaura state do SSR
if (window.__PINIA_STATE__) {
  pinia.state.value = JSON.parse(window.__PINIA_STATE__)
}

app.use(router).use(pinia)
await router.isReady()
app.mount('#app') // hydration (não recria o DOM)

Environment API (Vite 6) — ambientes client/server/custom

O Vite 6 introduziu a Environment API que permite isolar e configurar múltiplos ambientes no mesmo servidor dev — client, SSR server, Edge workers, etc. Cada ambiente tem seu próprio grafo de módulos e configuração de resolve.

// vite.config.ts — Vite 6+
import { defineConfig } from 'vite'

export default defineConfig({
  environments: {
    // Ambiente client — padrão
    client: {
      // configuração específica para o browser
      build: {
        outDir: 'dist/client',
      },
    },

    // Ambiente server — SSR Node.js
    server: {
      resolve: {
        // Condições de resolve para Node.js
        conditions: ['node', 'import', 'module'],
        noExternal: true,
      },
      build: {
        outDir: 'dist/server',
        ssr: true,
      },
    },

    // Ambiente customizado — Edge runtime (Cloudflare Workers, Deno)
    edge: {
      resolve: {
        conditions: ['workerd', 'worker', 'browser'],
      },
      build: {
        outDir: 'dist/edge',
        target: 'chrome96', // workerd/V8 version
      },
    },
  },
})
// Plugin acessando ambiente atual (Vite 6)
export function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    resolveId(id, importer, options) {
      // Detecta SSR pelo ambiente (substitui options.ssr deprecated)
      const isSSR = this.environment.config.consumer === 'server'
      if (isSSR) {
        // lógica server-side
      }
    },

    transform(code, id) {
      // Sabe em qual ambiente está sendo executado
      const env = this.environment.name // 'client' | 'server' | 'edge'
    },
  }
}

Testes com Vitest

Vitest é o test runner nativo do Vite — compartilha a configuração e os plugins do vite.config.ts. Compatível com a API do Jest.

// vite.config.ts — seção de test
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  test: {
    // globals: true — não precisa importar describe, it, expect
    globals: true,

    // Ambiente de DOM (jsdom ou happy-dom)
    environment: 'jsdom',

    // Setup file — importado antes de cada suite
    setupFiles: ['./src/test/setup.ts'],

    // CSS modules e arquivos de estilo
    css: true,

    // Coverage com v8 (padrão) ou istanbul
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov', 'html'],
      exclude: ['node_modules/', 'src/test/'],
      thresholds: {
        lines:    80,
        functions: 80,
        branches:  70,
      },
    },

    // Modo UI — interface visual no browser para rodar testes
    // npx vitest --ui
    ui: true,

    // Include/exclude de arquivos de teste
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    exclude: ['node_modules', 'dist', 'e2e'],
  },
})
// src/test/setup.ts — configuração global de testes
import { config } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'

// Configuração global do Vue Test Utils
config.global.plugins = [createTestingPinia()]

// src/components/__tests__/Counter.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'

describe('Counter', () => {
  it('renderiza o valor inicial', () => {
    const wrapper = mount(Counter, { props: { initial: 5 } })
    expect(wrapper.text()).toContain('5')
  })

  it('incrementa ao clicar', async () => {
    const wrapper = mount(Counter, { props: { initial: 0 } })
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })

  it('emite o evento correto', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('increment')).toBeTruthy()
    expect(wrapper.emitted('increment')?.[0]).toEqual([1])
  })
})

// Testando composables
import { useCounter } from '@/composables/useCounter'

describe('useCounter', () => {
  it('incrementa', () => {
    const { count, increment } = useCounter(0)
    increment()
    expect(count.value).toBe(1)
  })
})
# Comandos Vitest
npx vitest              # watch mode
npx vitest run          # executa uma vez (CI)
npx vitest --ui         # interface visual
npx vitest --coverage   # com coverage
npx vitest bench        # benchmarks

Otimizações — optimizeDeps, pre-bundling, chunk size

export default defineConfig({
  optimizeDeps: {
    // Incluir deps que o Vite não detectou automaticamente
    // (importadas dinamicamente ou fora do grafo principal)
    include: [
      'lodash-es',
      'firebase/app',
      'firebase/auth',
    ],

    // Excluir do pre-bundling (já são ESM, não precisam)
    exclude: ['@vueuse/core'],

    // holdUntilCrawlEnd (Vite 6, padrão true) — aguarda crawl completo
    // antes de fazer pre-bundle, evita multiplos re-bundles no start
    holdUntilCrawlEnd: true,

    // Forçar re-bundle mesmo com cache válido
    force: false, // --force na CLI para forçar
  },

  build: {
    // Não avisa sobre chunks grandes (aumenta limite)
    chunkSizeWarningLimit: 1000, // kB

    // Desativa inlining de assets (todos viram arquivos separados)
    assetsInlineLimit: 0,

    // Reportar tamanho comprimido (gzip)
    reportCompressedSize: true,
  },
})

Dicas de performance:

// 1. Dynamic import — routes e componentes pesados
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 2. shallowRef para dados grandes que você substitui inteiros
const bigDataset = shallowRef<DataRow[]>([])
bigDataset.value = await fetchAllRows() // uma substituição, não deep watch

// 3. Verificar tamanho do bundle
// npx vite build && npx rollup-plugin-visualizer

// 4. Otimizar dependências problemáticas
// moment.js — substituir por date-fns ou dayjs
// lodash — usar lodash-es com tree-shaking
// import isEqual from 'lodash-es/isEqual' // não: import _ from 'lodash'

Deploy — dist/, base path, hospedagem estática

// vite.config.ts — configurar base para subdirectory deploy
export default defineConfig({
  // Para GitHub Pages: https://user.github.io/repo-name/
  base: process.env.NODE_ENV === 'production' ? '/repo-name/' : '/',

  // Para domínio customizado ou root: base: '/'
  // base: '/', // padrão
})

GitHub Pages via GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci && npm run build
      - uses: actions/upload-pages-artifact@v3
        with: { path: dist }
      - uses: actions/deploy-pages@v4

Vercel e Netlify — configurações:

// vercel.json — SPA routing
{
  "rewrites": [
    { "source": "/((?!api/).*)", "destination": "/index.html" }
  ]
}
# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Docker + Nginx:

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# nginx.conf — SPA routing
server {
  listen 80;
  root /usr/share/nginx/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;  # SPA fallback
  }

  # Cache agressivo para assets com hash
  location ~* \.(js|css|png|jpg|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}

Tabela de versões Vite 2 → 7

VersãoAnoPrincipais novidades
Vite 22021Reescrita completa; SSR nativo; multi-framework via plugins; vite.config.ts; import.meta.env; esbuild para pre-bundling; import.meta.glob; hot module replacement por módulo
Vite 32022WebSockets para HMR (substitui EventSource); import.meta.glob v2 com queries (?raw, ?url, eager); modo inline para SSR; resolve.conditions; WebSockets para HMR mais estável; documentação reescrita; vite/client e vite/node separados
Vite 42022Rollup 3 internamente; novo parser de CSS; melhoria significativa em cold start; build.modulePreload polyfill configurável; resolução de assets em SSR melhorada; remove CJS Node API legado
Vite 52023Rollup 4 (partes em Rust) — build 40-70% mais rápido; import.meta.glob com eager e as; resolve.mainFields simplificado; suporte apenas Node 18+; remoção de APIs deprecated do v3/v4; optimizeDeps.holdUntilCrawlEnd
Vite 62024Environment API — ambientes client/SSR/worker isolados; buildApp para builds orquestradas; moduleRunner substitui ssrModuleLoader; suporte a ESM Runtime nativo em Node; environments na config; plugins podem acessar this.environment
Vite 72025Rolldown (Rollup em Rust) como bundler de produção — build 10-20× mais rápido; migração Rolldown transparente para a maioria dos projetos; @vitejs/plugin-vue e demais plugins atualizados para Rolldown

Vite 5 — Mudanças e Novidades

O Vite 5 foi lançado em novembro de 2023 com foco em performance de build, limpeza de APIs legadas e consistência entre desenvolvimento e produção.

Node.js mínimo 18

O suporte ao Node.js 14 e 16 foi removido. O requisito mínimo passou para Node.js 18. Na prática, projetos em Node 20+ (LTS atual) não são afetados.

node -v  # requer >= 18.0.0

Rollup 4 internamente

O Vite 5 adotou o Rollup 4 para os builds de produção. Rollup 4 tem partes reescritas em Rust (via SWC), tornando builds 40–70% mais rápidos em projetos de médio porte. A interface de plugins do Rollup 4 tem quebras de compatibilidade menores — plugins que usavam a hook options.plugins de forma não documentada podem precisar de atualização.

define — comportamento unificado dev/prod

No Vite 4, define substituía strings literalmente no código (comportamento herdado do webpack). No Vite 5, o define passou a usar o mesmo mecanismo do esbuild em ambos os modos (dev e produção), exigindo que os valores sejam JSON válido ou identificadores simples. Isso elimina inconsistências que quebravam builds em produção mas passavam em dev.

// Vite 5 — correto: sempre JSON.stringify strings e objetos
export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify('2.1.0'),
    __FEATURE_FLAGS__: JSON.stringify({ analytics: true, beta: false }),
    __BUILD_TIMESTAMP__: Date.now(),  // número — OK sem stringify
  },
})
// Vite 4 — funcionava mas era frágil (substituição textual)
// Isso podia quebrar em produção dependendo do contexto do token
define: {
  'process.env.NODE_ENV': '"production"',  // aspas duplas dentro de string
}

// Vite 5 — forma correta e consistente
define: {
  'process.env.NODE_ENV': JSON.stringify('production'),
}

resolve.browserField removido

A opção resolve.browserField (que controlava o uso do campo browser no package.json) foi removida. O comportamento padrão do Vite 5 é respeitar o campo browser automaticamente para o ambiente cliente, alinhado com o campo exports moderno do package.json. Use resolve.conditions para controlar as condições de resolução de módulos.

// Vite 4 — não mais necessário/disponível
resolve: {
  browserField: true,  // removido no Vite 5
}

// Vite 5 — controle via conditions
resolve: {
  conditions: ['browser', 'module', 'import', 'default'],
  // Para SSR/Node: ['node', 'module', 'import', 'default']
}

Outras mudanças notáveis do Vite 5

  • import.meta.glob com as renomeado para query: { as: 'raw' }{ query: '?raw' }
  • resolvePackageEntry e resolvePackageData removidos da API pública — use import-meta-resolve + vitefu
  • CJS Node API do Vite em modo legado removida — use ESM (import { createServer } from 'vite')
  • server.forceoptimizeDeps.force
  • optimizeDeps.holdUntilCrawlEnd: true por padrão — aguarda o crawl completo antes do pre-bundle, evitando múltiplos reloads no cold start
# Migrar um projeto Vite 4 para Vite 5
npm install vite@5 --save-dev

# Forçar re-bundle das deps após upgrade
npx vite --force

Tipagem de Variáveis de Ambiente

O arquivo src/vite-env.d.ts gerado pelo scaffolding do Vite serve para duas coisas: ativar os tipos globais do Vite (ImportMeta, import.meta.env, import.meta.glob etc.) e estender a interface ImportMetaEnv com as variáveis do projeto.

Estrutura do vite-env.d.ts

// src/vite-env.d.ts

// Referência obrigatória — ativa todos os tipos globais do Vite
/// <reference types="vite/client" />

// Extensão da interface com as variáveis VITE_ do projeto
interface ImportMetaEnv {
  // Variáveis obrigatórias (string sem undefined)
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_NAME: string
  readonly VITE_KEYCLOAK_URL: string
  readonly VITE_KEYCLOAK_REALM: string
  readonly VITE_KEYCLOAK_CLIENT_ID: string

  // Variáveis opcionais
  readonly VITE_SENTRY_DSN?: string
  readonly VITE_FEATURE_BETA?: string   // 'true' | 'false' — env vars são sempre string

  // Variáveis embutidas pelo Vite (já definidas em vite/client, aqui apenas para referência)
  // readonly MODE: string
  // readonly DEV: boolean
  // readonly PROD: boolean
  // readonly SSR: boolean
  // readonly BASE_URL: string
}

// Obrigatório para que a extensão de ImportMetaEnv seja reconhecida
interface ImportMeta {
  readonly env: ImportMetaEnv
}

Usar as variáveis com segurança de tipos

// TypeScript agora valida o acesso — erro em tempo de compilação se a var não existe
const apiUrl = import.meta.env.VITE_API_BASE_URL  // string
const dsn    = import.meta.env.VITE_SENTRY_DSN    // string | undefined

// Variável booleana — env vars são sempre string, fazer parse explícito
const isBeta = import.meta.env.VITE_FEATURE_BETA === 'true'

// Acessar variável desconhecida — erro TypeScript
// import.meta.env.VITE_DESCONHECIDA  // TS2339: Property does not exist

Prefixo customizado (envPrefix)

Por padrão somente variáveis com prefixo VITE_ são expostas ao cliente. Para bibliotecas internas ou monorepos, é possível alterar o prefixo:

// vite.config.ts
export default defineConfig({
  envPrefix: ['VITE_', 'PUBLIC_'],  // expõe VITE_* e PUBLIC_* ao cliente
})
// src/vite-env.d.ts — adicionar na interface
interface ImportMetaEnv {
  readonly PUBLIC_CDN_URL: string
  readonly VITE_API_URL: string
}

HMR API Customizada

A HMR API do Vite (import.meta.hot) permite que módulos controlem exatamente como respondem a atualizações de código em tempo de desenvolvimento. É injetada apenas em dev — em produção import.meta.hot é undefined e o dead code é removido pelo bundler.

Aceitar atualizações — accept()

accept() define o “hot boundary”: o Vite propaga a atualização até encontrar um módulo que a aceita, e re-executa o callback com o novo módulo.

// Aceitar atualizações do próprio módulo (self-accept)
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // newModule é o módulo recém-executado
    // undefined significa que o módulo foi invalidado
    if (newModule) {
      // Substituir a instância anterior pela nova
      app.replaceRenderer(newModule.default)
    }
  })
}
// Aceitar atualizações de dependências específicas
if (import.meta.hot) {
  import.meta.hot.accept('./config.ts', (newConfig) => {
    // Executado apenas quando config.ts muda
    updateAppConfig(newConfig?.default)
  })

  // Múltiplas dependências
  import.meta.hot.accept(
    ['./theme.ts', './translations.ts'],
    ([newTheme, newI18n]) => {
      if (newTheme) applyTheme(newTheme.default)
      if (newI18n)  updateTranslations(newI18n.messages)
    }
  )
}

Limpar side effects — dispose()

dispose() é chamado antes de aplicar a atualização. Use para cancelar timers, fechar conexões e evitar memory leaks. O objeto data persiste entre hot updates do mesmo módulo.

let socket: WebSocket | null = null

if (import.meta.hot) {
  // Restaurar estado persistido de um update anterior
  socket = import.meta.hot.data.socket ?? null

  import.meta.hot.dispose((data) => {
    // Persiste o socket para a próxima versão do módulo reutilizar
    data.socket = socket
    // Limpar o que não deve ser reutilizado
    clearInterval(import.meta.hot!.data.pollInterval)
  })

  import.meta.hot.accept((newModule) => {
    // newModule já recebeu o socket via data.socket
    newModule?.init()
  })
}

Invalidar o módulo — invalidate()

invalidate() força o Vite a propagar a atualização para cima na cadeia de importadores, como se o módulo não tivesse aceitado o update. Útil quando uma mudança requer que o módulo pai seja re-executado.

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Se a mudança alterou a interface pública, invalidar
    // para que os importadores também sejam atualizados
    if (newModule && publicAPIChanged(newModule)) {
      import.meta.hot!.invalidate('Interface pública alterada — reload do importador necessário')
      return
    }
    applyUpdate(newModule)
  })
}

Hot boundary — como o Vite propaga updates

Arquivo alterado: src/utils/format.ts

Vite verifica importadores de format.ts

src/components/Card.vue importa format.ts
  → Card.vue tem import.meta.hot.accept()?
        ↓ Não
src/App.vue importa Card.vue
  → App.vue tem import.meta.hot.accept()?
        ↓ Não
Vite chega à raiz → full page reload
// Card.vue — aceitar o update de format.ts evita o full reload
if (import.meta.hot) {
  import.meta.hot.accept('./utils/format', (newFormat) => {
    // Re-formata os dados exibidos no componente com a nova função
    rerender(newFormat?.formatCurrency)
  })
}

Lazy Loading e Code Splitting avançado

Dynamic import — import() nativo

O Vite transforma import() dinâmico em code splitting automático: cada módulo importado dinamicamente vira um chunk separado com <link rel="modulepreload"> gerado automaticamente.

// Componente carregado sob demanda (Vue)
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// Com loading state e fallback de erro
const DataTable = defineAsyncComponent({
  loader: () => import('./components/DataTable.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorFallback,
  delay: 200,     // ms antes de mostrar o loading
  timeout: 10000, // ms antes de considerar erro
})
// Rotas com lazy loading (Vue Router)
const routes = [
  {
    path: '/',
    component: () => import('./pages/Home.vue'),
  },
  {
    path: '/dashboard',
    // Prefetch hint — o browser baixa em background quando ocioso
    component: () => import(/* @vite-ignore */ './pages/Dashboard.vue'),
  },
  {
    path: '/admin',
    // Chunk nomeado — agrupa múltiplas rotas admin no mesmo chunk
    component: () => import(/* webpackChunkName: "admin" */ './pages/admin/Index.vue'),
  },
]

manualChunks — controle granular de chunks

manualChunks permite definir exatamente quais módulos vão para quais chunks, evitando que vendors diferentes se misturem ou que um único vendor engorde demais o bundle principal.

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // Forma de objeto — mapeamento explícito nome → módulos
        manualChunks: {
          'vendor-vue':    ['vue', 'vue-router', 'pinia'],
          'vendor-ui':     ['@headlessui/vue', '@heroicons/vue'],
          'vendor-charts': ['chart.js', 'vue-chartjs'],
          'vendor-utils':  ['date-fns', 'zod', 'axios'],
        },
      },
    },
  },
})
// Forma de função — lógica dinâmica (mais flexível)
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id: string) {
          // id é o caminho absoluto do módulo

          // Agrupar toda a pasta node_modules em sub-chunks por escopo
          if (id.includes('node_modules')) {
            // Pacotes @scope/name → chunk pelo escopo
            const match = id.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/)
            const pkg = match?.[1] ?? 'vendor'

            // Separar dependências críticas
            if (['vue', 'vue-router', 'pinia'].some(p => pkg.includes(p))) {
              return 'vendor-core'
            }
            if (pkg.includes('chart')) {
              return 'vendor-charts'
            }
            // Demais node_modules em chunk genérico
            return 'vendor'
          }

          // Agrupar módulos de uma feature em um único chunk
          if (id.includes('/src/modules/relatorios/')) {
            return 'feature-relatorios'
          }
          if (id.includes('/src/modules/admin/')) {
            return 'feature-admin'
          }
        },
      },
    },
  },
})

Preload de chunks — modulePreload

Vite injeta <link rel="modulepreload"> automaticamente para chunks que serão necessários logo. Para controlar o comportamento:

export default defineConfig({
  build: {
    modulePreload: {
      polyfill: true,  // injeta polyfill para browsers sem suporte nativo (padrão: true)
      // resolveDependencies — customizar quais chunks recebem preload
      resolveDependencies(url, deps, context) {
        // Retornar subconjunto de deps para fazer preload
        return deps.filter(dep => !dep.includes('vendor-charts'))
      },
    },
  },
})

vite-plugin-inspect

vite-plugin-inspect expõe uma interface visual para inspecionar o estado intermediário de cada plugin no pipeline de transformação do Vite. Indispensável para debugar plugins customizados, entender por que um módulo está sendo transformado de certa forma, ou identificar gargalos de performance.

Instalação e configuração

npm install -D vite-plugin-inspect
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Inspect from 'vite-plugin-inspect'

export default defineConfig({
  plugins: [
    vue(),
    // Apenas em dev — o plugin não faz nada em produção
    Inspect(),
  ],
})

Após iniciar o dev server, acesse http://localhost:5173/__inspect/ para ver a interface.

Interface do Inspect

A interface mostra três visões principais:

  • Modules — lista todos os módulos no grafo, com o tempo total de transformação de cada um
  • Plugins — lista todos os plugins e quantos módulos cada um transformou
  • Graph — grafo de dependências entre módulos
// Configuração avançada
Inspect({
  build: true,           // habilita também no build (gera relatório em dist/__inspect/)
  outputDir: '.vite-inspect',  // diretório de saída do relatório de build
  open: true,            // abre automaticamente no browser ao iniciar
  silent: false,         // logga a URL no terminal
})

Identificar gargalos via CLI

Alternativa ao plugin para diagnóstico rápido no terminal:

# Ver tempo de transformação de cada arquivo
vite --debug plugin-transform 2>&1 | grep "vite:transform"

# Saída típica:
# vite:transform  28ms  /@vite/client
# vite:transform  62ms  /src/components/BigComponent.vue
# vite:transform 102ms  /src/utils/big-utils.js

Arquivos com tempo alto são candidatos para pré-aquecimento via server.warmup:

export default defineConfig({
  server: {
    warmup: {
      // Pré-transforma esses arquivos no startup, antes da primeira requisição
      clientFiles: [
        './src/components/BigComponent.vue',
        './src/utils/big-utils.js',
      ],
    },
  },
})

Performance

build.minify — opções de minificação

export default defineConfig({
  build: {
    // 'esbuild' — padrão, muito rápido, boa compressão
    minify: 'esbuild',

    // 'terser' — compressão ligeiramente melhor (~5-10%), build mais lento
    // Requer: npm install -D terser
    // minify: 'terser',

    // false — sem minificação (útil para debug de bundle)
    // minify: false,

    // Opções do esbuild (quando minify: 'esbuild')
    esbuildOptions: {
      drop: ['console', 'debugger'],  // remove console.* e debugger em produção
      legalComments: 'none',          // remove comentários de licença (cuidado!)
      target: 'es2022',
    },

    // Opções do Terser (quando minify: 'terser')
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log', 'console.info'],
      },
      mangle: {
        safari10: true,  // workaround para bug no Safari 10
      },
    },
  },
})

build.cssMinify — minificação de CSS separada

A partir do Vite 4.4, é possível controlar a minificação do CSS independentemente do JS:

export default defineConfig({
  build: {
    minify: 'esbuild',       // JS com esbuild
    cssMinify: 'lightningcss', // CSS com lightningcss (mais rápido e moderno)
    // cssMinify: 'esbuild', // padrão
    // cssMinify: false,     // sem minificação de CSS
  },
})

Para usar lightningcss como processador CSS completo (substituindo PostCSS):

npm install -D lightningcss
export default defineConfig({
  css: {
    transformer: 'lightningcss',
    lightningcss: {
      targets: browserslistToTargets(browserslist('>= 0.25%')),
      drafts: {
        nesting: true,        // CSS nesting nativo
        customMedia: true,    // @custom-media
      },
    },
  },
  build: {
    cssMinify: 'lightningcss',
  },
})

optimizeDeps.include e optimizeDeps.exclude

O pre-bundling do Vite escaneia o código para detectar dependências automaticamente. Em casos de importações dinâmicas ou dependências fora do grafo principal, pode ser necessário incluir/excluir manualmente:

export default defineConfig({
  optimizeDeps: {
    // include — forçar pre-bundle de deps não detectadas automaticamente
    include: [
      // Deps importadas dinamicamente (o scan estático não as encontra)
      'firebase/auth',
      'firebase/firestore',
      'firebase/storage',

      // Deps com muitos sub-módulos CJS (evitar re-bundles durante dev)
      'lodash',
      '@aws-sdk/client-s3',

      // Deep imports que o Vite não detecta no scan inicial
      'date-fns/formatDistanceToNow',
    ],

    // exclude — não fazer pre-bundle (já são ESM puros e bem estruturados)
    exclude: [
      '@vueuse/core',   // ESM puro, tree-shaking funciona bem sem pre-bundle
      'vue-demi',
    ],

    // force — ignorar cache e re-executar o pre-bundle
    // Útil após atualizar uma dep com comportamento estranho
    // force: true,  // use apenas quando necessário — mais lento no startup
  },
})
# Forçar re-bundle via CLI (equivale a force: true, mas temporário)
npx vite --force

Quando usar include:

  • Dependência importada dentro de um if (condition) ou import() dinâmico não rastreável estaticamente
  • Deps que o Vite “descobre” tardiamente causando reload inesperado no primeiro uso
  • Pacotes CJS com muitos arquivos internos que tornam o dev server lento

Quando usar exclude:

  • Pacotes já em formato ESM que são re-exportados por um wrapper (ex: vue-demi)
  • Monorepos onde o pacote local precisa ser resolvido ao vivo sem cache