O problema com o bundle padrão
Vite gera um bundle otimizado por padrão, mas sem configuração manual, bibliotecas grandes como framer-motion, lucide-react e react-query acabam no mesmo chunk que o código da aplicação — o que significa que uma mudança pequena invalida o cache de tudo.
Manual chunks: separando vendor do app
A chave para cache-busting eficiente é separar dependências (que mudam raramente) do código da aplicação (que muda sempre). O Rollup — engine do Vite em produção — expõe manualChunks exatamente para isso.
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
router: ["react-router-dom"],
query: ["@tanstack/react-query"],
motion: ["framer-motion"],
icons: ["lucide-react"],
ui: [
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
// ... outros radix
],
},
},
},
},
});Separe ícones em chunk próprio. lucide-react tem centenas de exports e mesmo com tree-shaking pode ser grande dependendo de quantos você usa.
Analisando o bundle com rollup-plugin-visualizer
Antes de otimizar, meça. O rollup-plugin-visualizer gera um treemap interativo que mostra exatamente quanto espaço cada módulo ocupa.
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
visualizer({
open: true, // abre no browser automaticamente
filename: "dist/stats.html",
gzipSize: true,
brotliSize: true,
}),
],
});Compressão Brotli + Gzip
O Vite não gera arquivos comprimidos por padrão — deixa para o servidor fazer on-the-fly. Pré-comprimir em build time é mais eficiente: o servidor serve o arquivo já comprimido sem CPU extra por request.
import viteCompression from "vite-plugin-compression";
export default defineConfig({
plugins: [
viteCompression({ algorithm: "brotliCompress", ext: ".br" }),
viteCompression({ algorithm: "gzip", ext: ".gz" }),
],
});Resultado
- Bundle total: 847KB → 512KB (39% menor após Brotli)
- Cache hit rate: subiu de ~30% para ~85% com chunk separation
- First load JS: 243KB → 148KB (chunks lazy-loaded separados)
- LCP: 2.8s → 1.1s em mobile (com lazy loading de rotas + imagens)
Números do Lighthouse simulando conexão 4G lenta. Resultados variam, mas o padrão de melhora com code splitting + compressão é consistente.