1.7 — Build de Produção e Otimização
Gere builds otimizados, analise o bundle e aplique técnicas de performance.
Objetivos da Aula
- Entender o processo de build com Rollup
- Analisar o tamanho e composição do bundle
- Aplicar técnicas de code splitting
- Otimizar assets (imagens, fontes, CSS)
O Processo de Build
Quando você roda npm run build, o Vite usa o Rollup para criar bundles otimizados:
PROCESSO DE BUILD
npm run build
▼
1
Resolucao de Modulos
- Analisa imports
- Constroi grafo de dependencias
▼
2
Transformacao
- TypeScript → JavaScript
- JSX → JavaScript
- CSS → CSS otimizado
▼
3
Tree Shaking
- Remove codigo nao usado
- Elimina imports mortos
▼
4
Minificacao (esbuild)
- Remove espacos/comentarios
- Encurta nomes de variaveis
- Otimiza codigo
▼
5
Code Splitting
- Divide em chunks
- Adiciona hashes para cache
▼
dist/
index.html
assets/
index-[hash].js
index-[hash].css
vendor-[hash].js
Analisando o Build
Output Padrão
npm run build
# Output:
vite v5.0.0 building for production...
✓ 42 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-DiwrgTda.css 1.24 kB │ gzip: 0.65 kB
dist/assets/index-D8mTLhPd.js 1.45 kB │ gzip: 0.75 kB
✓ built in 523ms Visualizando o Bundle
Instale o visualizer:
npm install rollup-plugin-visualizer -D // vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true, // Abre automaticamente
filename: 'stats.html', // Nome do arquivo
gzipSize: true, // Mostra tamanho gzip
brotliSize: true // Mostra tamanho brotli
})
]
}) Rode npm run build e um gráfico interativo abrirá no navegador!
Estrutura do dist/
tree dist/
# dist/
# ├── index.html
# ├── vite.svg
# └── assets/
# ├── index-DiwrgTda.css # CSS minificado
# ├── index-D8mTLhPd.js # JS principal
# └── vendor-abc123.js # Dependências
# Verificar tamanhos
du -sh dist/*
du -sh dist/assets/* Tree Shaking
Tree shaking remove código que nunca é usado:
// utils.js
export function soma(a, b) { return a + b }
export function subtracao(a, b) { return a - b }
export function multiplicacao(a, b) { return a * b }
export function divisao(a, b) { return a / b }
// main.js
import { soma } from './utils.js'
console.log(soma(1, 2))
// No build final, APENAS soma() está incluída!
// subtracao, multiplicacao, divisao são removidas. Requisitos para Tree Shaking
// ✅ Funciona - ESModules
export function foo() {}
import { foo } from './module'
// ❌ Não funciona - CommonJS
module.exports = { foo }
const { foo } = require('./module') Marcando Pacotes como Side-Effect Free
// package.json
{
"sideEffects": false
}
// ou específico
{
"sideEffects": ["*.css", "*.scss"]
} Code Splitting
Automático por Rota
O Vite faz code splitting automático para imports dinâmicos:
// Carregado imediatamente
import { Header } from './components/Header.js'
// Carregado sob demanda (lazy loading)
const AdminPanel = () => import('./components/AdminPanel.js')
const Settings = () => import('./components/Settings.js')
// Uso
if (isAdmin) {
const { default: AdminPanel } = await AdminPanel()
// AdminPanel está em um chunk separado!
} Manual Chunks
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Agrupa lodash em chunk separado
lodash: ['lodash-es'],
// Agrupa bibliotecas de UI
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown'],
// Agrupa bibliotecas de gráficos
charts: ['chart.js', 'd3']
}
}
}
}
}) Função para Chunks Dinâmicos
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Separa node_modules
if (id.includes('node_modules')) {
// Bibliotecas grandes separadas
if (id.includes('lodash')) return 'vendor-lodash'
if (id.includes('chart')) return 'vendor-charts'
if (id.includes('@radix')) return 'vendor-radix'
// Resto em vendor comum
return 'vendor'
}
// Componentes compartilhados
if (id.includes('/components/shared/')) {
return 'shared'
}
}
}
}
}
}) Otimização de Assets
Imagens
// vite.config.js
export default defineConfig({
build: {
// Imagens menores que 4KB viram base64 inline
assetsInlineLimit: 4096,
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
// Organiza por tipo
const ext = assetInfo.name.split('.').pop()
if (/png|jpe?g|svg|gif|webp|avif/.test(ext)) {
return 'images/[name]-[hash][extname]'
}
if (/woff2?|eot|ttf|otf/.test(ext)) {
return 'fonts/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
}
}
}
}
}) Importando Imagens Otimizadas
// Import como URL (será otimizada)
import logo from './logo.png'
img.src = logo // /assets/logo-abc123.png
// Import com query para controle
import logoUrl from './logo.png?url' // Sempre URL
import logoRaw from './logo.png?raw' // Conteúdo bruto
import logoInline from './logo.png?inline' // Sempre base64 CSS
// vite.config.js
export default defineConfig({
build: {
cssCodeSplit: true, // CSS separado por chunk (default)
// cssCodeSplit: false // Todo CSS em um arquivo
cssMinify: 'esbuild', // ou 'lightningcss' (mais rápido)
},
css: {
// PostCSS com autoprefixer e cssnano
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true }
}]
})
]
}
}
}) Compressão
Plugin de Compressão
npm install vite-plugin-compression -D // vite.config.js
import compression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
// Gzip
compression({
algorithm: 'gzip',
ext: '.gz',
threshold: 1024 // Apenas arquivos > 1KB
}),
// Brotli (melhor compressão)
compression({
algorithm: 'brotliCompress',
ext: '.br',
threshold: 1024
})
]
}) Resultado:
dist/assets/
├── index-abc123.js (50 KB)
├── index-abc123.js.gz (15 KB) # ~70% menor
├── index-abc123.js.br (12 KB) # ~76% menor Configurar Servidor para Servir Comprimido
# nginx.conf
gzip_static on;
brotli_static on; Sourcemaps
Opções de Sourcemap
// vite.config.js
export default defineConfig({
build: {
// Opções de sourcemap
sourcemap: true, // Arquivo .map separado
sourcemap: 'inline', // Inline no JS (maior)
sourcemap: 'hidden', // .map existe mas não referenciado
sourcemap: false // Sem sourcemap (menor)
}
}) Recomendação por Ambiente
export default defineConfig(({ mode }) => ({
build: {
// Dev/staging: sourcemaps completos
// Production: hidden (upload para serviço de erros)
sourcemap: mode === 'production' ? 'hidden' : true
}
})) Medindo Performance
Lighthouse CI
npm install -g @lhci/cli
# Após o build
npm run build
npm run preview &
lhci autorun Script de Análise
// scripts/analyze-build.js
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
import { gzipSync } from 'zlib'
import { readFileSync } from 'fs'
async function analyzeBuild(dir = 'dist') {
const files = await readdir(join(dir, 'assets'))
let totalSize = 0
let totalGzip = 0
console.log('\n📊 Análise do Build\n')
console.log('Arquivo'.padEnd(40), 'Tamanho'.padEnd(12), 'Gzip')
console.log('─'.repeat(65))
for (const file of files) {
const path = join(dir, 'assets', file)
const content = readFileSync(path)
const size = content.length
const gzipped = gzipSync(content).length
totalSize += size
totalGzip += gzipped
console.log(
file.padEnd(40),
formatBytes(size).padEnd(12),
formatBytes(gzipped)
)
}
console.log('─'.repeat(65))
console.log(
'TOTAL'.padEnd(40),
formatBytes(totalSize).padEnd(12),
formatBytes(totalGzip)
)
}
function formatBytes(bytes) {
return (bytes / 1024).toFixed(2) + ' KB'
}
analyzeBuild() 🎯 Mini-Projeto: Build Otimizado
Vamos otimizar nosso Dashboard para produção:
Passo 1: Configuração Otimizada
// vite.config.js
import { defineConfig } from 'vite'
import path from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import compression from 'vite-plugin-compression'
export default defineConfig(({ mode }) => {
const isProd = mode === 'production'
return {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
}
},
build: {
// Sourcemaps apenas em staging
sourcemap: mode === 'staging',
// Target moderno para bundles menores
target: 'esnext',
// Limite de warning
chunkSizeWarningLimit: 500,
// Organização de assets
rollupOptions: {
output: {
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'css/[name]-[hash][extname]'
}
if (/.(png|jpg|jpeg|gif|svg|webp)$/.test(assetInfo.name)) {
return 'images/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
},
// Chunks manuais
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
},
plugins: [
// Visualização do bundle (só quando ANALYZE=true)
process.env.ANALYZE && visualizer({
open: true,
filename: 'bundle-analysis.html',
gzipSize: true
}),
// Compressão em produção
isProd && compression({ algorithm: 'gzip' }),
isProd && compression({ algorithm: 'brotliCompress', ext: '.br' })
].filter(Boolean),
server: {
port: 3000,
open: true
}
}
}) Passo 2: Scripts de Build
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:staging": "vite build --mode staging",
"build:analyze": "ANALYZE=true vite build",
"preview": "vite preview",
"size": "npm run build && du -sh dist dist/assets/*"
}
} Passo 3: Componente de Info do Build
// src/components/BuildInfo.js
import { env } from '@/config/env.js'
export function createBuildInfo() {
// Só mostra em desenvolvimento ou staging
if (env.runtime.isProd && !env.flags.debug) {
return null
}
const info = document.createElement('div')
info.className = 'build-info'
info.innerHTML = `
<details>
<summary>🔧 Build Info</summary>
<ul>
<li><strong>Ambiente:</strong> ${env.runtime.environment}</li>
<li><strong>Modo:</strong> ${env.runtime.mode}</li>
<li><strong>Versão:</strong> ${env.app.version}</li>
<li><strong>Build:</strong> ${__BUILD_TIME__ || 'N/A'}</li>
</ul>
</details>
`
return info
} Passo 4: Testar
# Build normal
npm run build
# Build com análise
npm run build:analyze
# Abrirá gráfico do bundle
# Verificar tamanhos
npm run size
# Preview de produção
npm run preview ✅ Desafio da Aula
Objetivo
Reduzir o tamanho do bundle adicionando e depois otimizando uma dependência pesada.
Instruções
- Instale
lodash-es:npm install lodash-es - Use apenas a função
debounceno código - Verifique que APENAS
debounceestá no bundle (tree shaking) - Configure um chunk separado para lodash
Spec de Verificação
-
npm run build:analyzemostra lodash em chunk separado - O tamanho do chunk de lodash é < 5KB (só debounce)
- O app funciona normalmente com debounce
Solução
🔍 Clique para ver a solução
npm install lodash-es // src/utils/debounce.js
// Importa APENAS debounce, não todo o lodash
import { debounce } from 'lodash-es'
export { debounce } // src/main.js
import { debounce } from '@utils/debounce.js'
// Usar debounce para atualizar métricas
const atualizarMetricas = debounce(() => {
// ... código
}, 500) // vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
lodash: ['lodash-es']
}
}
}
}
}) Após build:
dist/assets/
├── index-xxx.js (~2 KB)
├── lodash-xxx.js (~4 KB) # Apenas debounce!
└── index-xxx.css (~1 KB) Se lodash estivesse completo, seria ~70KB!
Próxima aula: 1.8 — Vite para Diferentes Frameworks