2.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 pnpm build, o Vite usa o Rollup para criar bundles otimizados:
PROCESSO DE BUILD
pnpm 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
pnpm 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:
pnpm add -D rollup-plugin-visualizer // vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
// Abre automaticamente
open: true,
// Nome do arquivo
filename: 'stats.html',
// Mostra tamanho gzip
gzipSize: true,
// Mostra tamanho brotli
brotliSize: true
})
]
}) Rode pnpm 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:
ESModules obrigatorios
Tree shaking so funciona com ESModules (`import`/`export`). Se a dependencia usa CommonJS (`require`/`module.exports`), o Rollup nao consegue eliminar codigo morto. Sempre prefira pacotes que exportam ESM.
// utils.ts
export function soma(a: number, b: number): number {
return a + b
}
export function subtracao(a: number, b: number): number {
return a - b
}
export function multiplicacao(a: number, b: number): number {
return a * b
}
export function divisao(a: number, b: number): number {
return a / b
}
// main.ts
import { soma } from './utils'
console.log(soma(1, 2))
// No build final, APENAS soma() esta incluida!
// subtracao, multiplicacao, divisao sao removidas. Requisitos para Tree Shaking
// Funciona - ESModules
export function foo() {}
import { foo } from './module'
// Nao 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
O que e code splitting e por que importa?
Code splitting divide o bundle em pedacos menores (chunks) que sao carregados sob demanda. Isso reduz o tempo de carregamento inicial porque o navegador so baixa o codigo necessario para a pagina atual. Paginas ou funcionalidades que o usuario ainda nao acessou ficam em chunks separados, carregados apenas quando necessario.
Automatico por Rota
O Vite faz code splitting automatico para imports dinamicos:
// Carregado imediatamente
import Header from './components/Header.svelte'
// Carregado sob demanda (lazy loading)
// Svelte usa import() dinamico para componentes
const AdminPanel = () => import('./components/AdminPanel.svelte')
const Settings = () => import('./components/Settings.svelte')
// Uso com await
if (isAdmin) {
const { default: AdminPanel } = await AdminPanel()
// AdminPanel esta em um chunk separado!
} Manual Chunks
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Agrupa lodash em chunk separado
lodash: ['lodash-es'],
// Agrupa bibliotecas de UI Svelte
ui: ['bits-ui', 'melt-ui'],
// Agrupa bibliotecas de graficos
charts: ['chart.js', 'd3']
}
}
}
}
}) Funcao para Chunks Dinamicos
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id: string): string | undefined {
// 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('bits-ui')) return 'vendor-ui'
// Resto em vendor comum
return 'vendor'
}
// Componentes compartilhados
if (id.includes('/components/shared/')) {
return 'shared'
}
}
}
}
}
}) Otimização de Assets
Imagens
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// Imagens menores que 4KB viram base64 inline
assetsInlineLimit: 4096,
rollupOptions: {
output: {
assetFileNames: (assetInfo: { name: string }) => {
// 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 (sera otimizada)
import logo from './logo.png'
// logo = /assets/logo-abc123.png
// Import com query para controle
// Sempre URL
import logoUrl from './logo.png?url'
// Conteudo bruto
import logoRaw from './logo.png?raw'
// Sempre base64
import logoInline from './logo.png?inline' CSS
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// CSS separado por chunk (default)
cssCodeSplit: true,
// cssCodeSplit: false
// Todo CSS em um arquivo
// ou 'lightningcss' (mais rapido)
cssMinify: 'esbuild',
},
css: {
// PostCSS com autoprefixer e cssnano
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true }
}]
})
]
}
}
}) Compressão
Plugin de Compressão
pnpm add -D vite-plugin-compression // vite.config.ts
import { defineConfig } from 'vite'
import compression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
// Gzip
compression({
algorithm: 'gzip',
ext: '.gz',
// Apenas arquivos > 1KB
threshold: 1024
}),
// Brotli (melhor compressao)
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; Brotli vs Gzip
Brotli oferece compressao de 15-20% melhor que gzip para assets web. Todos os navegadores modernos suportam Brotli via HTTPS. Use ambos para garantir compatibilidade: Brotli como primeira opcao e gzip como fallback.
Sourcemaps
Opcoes de Sourcemap
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// Opcoes de sourcemap:
// true = Arquivo .map separado
// 'inline' = Inline no JS (maior)
// 'hidden' = .map existe mas nao referenciado
// false = Sem sourcemap (menor)
sourcemap: true
}
}) Recomendacao por Ambiente
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => ({
build: {
// Dev/staging: sourcemaps completos
// Production: hidden (upload para servico de erros)
sourcemap: mode === 'production' ? 'hidden' : true
}
})) Devo usar sourcemaps em producao?
Depende. Sourcemaps facilitam o debug de erros em producao, mas expoem seu codigo-fonte. A melhor pratica e usar `'hidden'`: os arquivos `.map` sao gerados mas nao referenciados no bundle. Voce faz upload dos `.map` para um servico como Sentry e remove os arquivos do servidor publico.
Medindo Performance
Lighthouse CI
pnpm add -g @lhci/cli
# Após o build
pnpm build
pnpm preview &
lhci autorun Script de Analise
// scripts/analyze-build.ts
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
import { gzipSync } from 'zlib'
import { readFileSync } from 'fs'
interface FileInfo {
name: string
size: number
gzipped: number
}
async function analyzeBuild(
dir: string = 'dist'
): Promise<void> {
const files = await readdir(join(dir, 'assets'))
let totalSize = 0
let totalGzip = 0
console.log('\nAnalise do Build\n')
console.log(
'Arquivo'.padEnd(40),
'Tamanho'.padEnd(12),
'Gzip'
)
console.log('-'.repeat(65))
for (const file of files) {
const filePath = join(dir, 'assets', file)
const content: Buffer = readFileSync(filePath)
const size: number = content.length
const gzipped: number = 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: number): string {
return (bytes / 1024).toFixed(2) + ' KB'
}
analyzeBuild() Mini-Projeto: Build Otimizado
Vamos otimizar nosso Dashboard para producao:
Passo 1: Configuracao Otimizada
// vite.config.ts
import { defineConfig } from 'vite'
import { sveltekit } from '@sveltejs/kit/vite'
import path from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import compression from 'vite-plugin-compression'
export default defineConfig(({ mode }) => {
const isProd: boolean = 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,
// Organizacao de assets
rollupOptions: {
output: {
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
assetFileNames: (
assetInfo: { name: string }
) => {
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: string
): string | undefined {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
},
plugins: [
sveltekit(),
// Visualizacao do bundle
// (so quando ANALYZE=true)
process.env.ANALYZE && visualizer({
open: true,
filename: 'bundle-analysis.html',
gzipSize: true
}),
// Compressao em producao
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": "pnpm build && du -sh dist dist/assets/*"
}
} Passo 3: Componente de Info do Build
// src/components/BuildInfo.ts
import { env } from '@/config/env'
interface BuildInfoElement extends HTMLDivElement {}
export function createBuildInfo(): BuildInfoElement | null {
// So mostra em desenvolvimento ou staging
if (env.runtime.isProd && !env.flags.debug) {
return null
}
const info = document.createElement('div') as BuildInfoElement
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>Versao:</strong> ${env.app.version}</li>
<li><strong>Build:</strong> ${__BUILD_TIME__ || 'N/A'}</li>
</ul>
</details>
`
return info
} Passo 4: Testar
# Build normal
pnpm build
# Build com análise
pnpm build:analyze
# Abrirá gráfico do bundle
# Verificar tamanhos
pnpm size
# Preview de produção
pnpm preview Desafio da Aula
Objetivo
Reduzir o tamanho do bundle adicionando e depois otimizando uma dependência pesada.
Instrucoes
- Instale
lodash-es:pnpm add lodash-es - Use apenas a funcao
debounceno codigo - Verifique que APENAS
debounceesta no bundle (tree shaking) - Configure um chunk separado para lodash
Spec de Verificacao
-
pnpm build:analyzemostra lodash em chunk separado - O tamanho do chunk de lodash é < 5KB (so debounce)
- O app funciona normalmente com debounce
Solucao
Clique para ver a solucao
pnpm add lodash-es // src/utils/debounce.ts
// Importa APENAS debounce, nao todo o lodash
import { debounce } from 'lodash-es'
export { debounce } // src/main.ts
import { debounce } from '@utils/debounce'
// Usar debounce para atualizar metricas
const atualizarMetricas = debounce((): void => {
// ... codigo
}, 500) // vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
lodash: ['lodash-es']
}
}
}
}
}) Apos 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: 2.8 — Vite para Diferentes Frameworks