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 devEstrutura 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 commitarO 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.tsTipagem 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 # benchmarksOtimizaçõ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@v4Vercel 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 = 200Docker + 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ão | Ano | Principais novidades |
|---|---|---|
| Vite 2 | 2021 | Reescrita 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 3 | 2022 | WebSockets 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 4 | 2022 | Rollup 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 5 | 2023 | Rollup 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 6 | 2024 | Environment 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 7 | 2025 | Rolldown (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.0Rollup 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.globcomasrenomeado paraquery:{ as: 'raw' }→{ query: '?raw' }resolvePackageEntryeresolvePackageDataremovidos da API pública — useimport-meta-resolve+vitefu- CJS Node API do Vite em modo legado removida — use ESM (
import { createServer } from 'vite') server.force→optimizeDeps.forceoptimizeDeps.holdUntilCrawlEnd: truepor 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 --forceTipagem 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 existPrefixo 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.jsArquivos 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 lightningcssexport 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 --forceQuando usar include:
- Dependência importada dentro de um
if (condition)ouimport()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