2.2 — Arquitetura do Vite

Entenda como o Vite funciona internamente: servidor de desenvolvimento, pré-bundling e o papel do esbuild e Rollup.

Objetivos da Aula

  • Compreender a arquitetura interna do servidor de desenvolvimento
  • Entender o processo de pré-bundling de dependências com esbuild
  • Conhecer o fluxo de transformação de arquivos
  • Diferenciar o funcionamento em desenvolvimento vs produção

Visão Geral da Arquitetura

O Vite possui duas arquiteturas distintas que trabalham em conjunto:

ARQUITETURA DO VITE

DESENVOLVIMENTO
  • Servidor ESM
  • + esbuild
  • + HMR nativo
pnpm dev
PRODUÇÃO
  • Rollup Bundler
  • + Plugins
  • + Otimizações
pnpm build

O Servidor de Desenvolvimento

Fluxo de uma Requisição

Quando você acessa http://localhost:5173, acontece o seguinte:

FLUXO DE REQUISIÇÃO

1
GET /
Serve index.html
Navegador encontra <script type="module" src="/src/main.ts">
2
GET /src/main.ts
Transforma main.ts sob demanda
Navegador recebe main.ts, encontra: import './app.ts'
3
GET /src/app.ts
Transforma app.ts sob demanda
E assim por diante para cada import...

Código Exemplo: Vendo na Prática

Crie estes arquivos no seu projeto:

// src/main.ts
import { saudacao } from './utils/saudacao'
import { formataData } from './utils/data'

console.log(saudacao('Mundo'))
console.log(formataData(new Date()))
// src/utils/saudacao.ts
export function saudacao(nome: string): string {
  return `Olá, ${nome}!`
}
// src/utils/data.ts
export function formataData(data: Date): string {
  return data.toLocaleDateString('pt-BR')
}

Abra o DevTools do navegadorNetwork e observe:

Request 1: /src/main.ts         (200 OK)
Request 2: /src/utils/saudacao.ts (200 OK)
Request 3: /src/utils/data.ts     (200 OK)

Cada arquivo é uma requisição separada! O navegador resolve os imports.


Pré-bundling de Dependências

O que é pré-bundling?
Pré-bundling é o processo onde o Vite converte dependências do node_modules em módulos ESM otimizados antes de iniciar o servidor. Isso resolve dois problemas: bibliotecas com centenas de arquivos internos (que causariam centenas de requisições HTTP) e bibliotecas que usam CommonJS (que o navegador nao suporta nativamente).

O Problema com node_modules

Nem todas as dependências funcionam bem com ESModules nativos:

// lodash tem centenas de módulos internos
import { debounce } from 'lodash-es'
// Isso causaria CENTENAS de requisições HTTP!

// algumas libs usam CommonJS internamente
import dayjs from 'dayjs'
// CommonJS não funciona direto no navegador!

A Solução: esbuild

O Vite usa o esbuild para pré-compilar dependências no primeiro start:

PRÉ-BUNDLING

Primeiro pnpm dev:

node_modules/
lodash-es/
debounce.js
throttle.js
...300+ files
dayjs/
index.js
locale/
...
node_modules/.vite/deps/
lodash-es.js (1 arquivo)
dayjs.js (1 arquivo)
(CommonJS → ESM)
⚡ esbuild faz isso em ~100ms

Vendo o Cache

Após rodar pnpm dev, observe a pasta criada:

ls node_modules/.vite/deps/

Você verá arquivos como:

_metadata.json
lodash-es.js
lodash-es.js.map
dayjs.js
dayjs.js.map

Reescrita de Imports

O Vite reescreve seus imports automaticamente:

// Seu código (o que você escreve):
import { debounce } from 'lodash-es'

// O que o navegador recebe:
import { debounce } from '/node_modules/.vite/deps/lodash-es.js?v=abc123'

Hot Module Replacement (HMR)

Por que o Vite usa WebSockets para HMR?
O protocolo HTTP tradicional funciona no modelo request-response: o cliente pede, o servidor responde. Com WebSockets, o servidor pode enviar mensagens ao navegador a qualquer momento, sem que o cliente precise ficar perguntando "mudou algo?". Isso permite que o Vite notifique o navegador instantaneamente quando um arquivo muda no disco.

Como Funciona

O HMR do Vite usa WebSockets para comunicação instantânea:

HMR FLOW

1
Você edita: src/components/Button.svelte
2
Vite detecta a mudança (chokidar file watcher)
3
Vite envia via WebSocket:
{ type: 'update', updates: [{
  path: '/src/components/Button.svelte',
  timestamp: 1699123456789
}] }
4
Cliente Vite (no navegador):
  • Recebe a mensagem
  • Faz import() dinâmico do módulo atualizado
  • Substitui o módulo antigo pelo novo
  • Estado da aplicação é PRESERVADO!
⏲ Tempo total: 10-20ms

Exemplo: Observando o HMR

Abra o DevTools → Console e observe as mensagens:

[vite] connecting...
[vite] connected.

# Quando você edita um arquivo:
[vite] hot updated: /src/main.ts

API do HMR

Você pode interagir com o HMR programaticamente:

// main.ts
import { contador } from './contador'

// tipagem para o contador
const valor: number = contador

console.log('Contagem:', valor)

// API de HMR do Vite
if (import.meta.hot) {
  // Aceita atualizações deste módulo
  import.meta.hot.accept()

  // Executa quando o módulo é substituído
  import.meta.hot.dispose(() => {
    console.log('Módulo antigo sendo descartado')
  })
}

Transformação de Arquivos

Pipeline de Transformação

PIPELINE DE TRANSFORMAÇÃO

Arquivo Fonte
Resolve Imports
(ex: 'lodash')
Reescreve bare imports para caminhos válidos
Plugins
(opcional)
Plugins transformam o código (TypeScript, JSX, Vue, Svelte)
esbuild
(se TS/JSX)
Transpila TS/JSX se necessário (10-100x mais rápido que Babel)
Código ESM pronto para o navegador

Exemplo: TypeScript

// src/utils/math.ts
export function soma(a: number, b: number): number {
  return a + b
}

O Vite transforma para:

// O que o navegador recebe:
export function soma(a, b) {
  return a + b
}

A transformação acontece sob demanda, não antecipadamente!


Arquitetura de Produção

No build de produção, o Vite usa Rollup:

BUILD DE PRODUÇÃO

pnpm build

src/
main.ts
app.ts
utils/
styles.css
dist/
index.html
assets/
main-a1b2c3.js
style-d4e5f6.css
vite.svg
Rollup aplica:
✓ Tree-shaking (remove código não usado)
✓ Minificação (reduz tamanho)
✓ Code splitting (divide em chunks)
✓ Hashing (cache busting)

Por que Rollup e não esbuild para produção?

Por que nao usar esbuild para tudo?
O esbuild é incrivelmente rapido, mas ainda nao oferece code splitting tao maduro quanto o Rollup. O Rollup tem um ecossistema de plugins muito mais robusto e otimizacoes avancadas para producao. Como o build de producao acontece apenas ocasionalmente (nao a cada salvamento), a velocidade menor do Rollup nao impacta a experiencia do desenvolvedor.
esbuild:
  Extremamente rápido
  Code splitting ainda não é ideal
  Plugins menos flexíveis

Rollup:
  Code splitting maduro
  Ecossistema de plugins robusto
  Otimizações avançadas
  Mais lento (mas OK para builds ocasionais)

Exemplo Prático: Observando a Arquitetura

1. Veja o pré-bundling acontecendo

# Delete o cache
rm -rf node_modules/.vite

# Rode o dev server e observe o terminal
pnpm dev

Você verá:

Optimizing dependencies:
  lodash-es, dayjs, svelte
Pre-bundling them to speed up dev server page load...

2. Compare dev vs build

# Em desenvolvimento
pnpm dev
# Abra DevTools → Network
# observe dezenas de arquivos .ts

# Para build
pnpm build
# Observe a pasta dist/
# poucos arquivos otimizados

3. Inspecione o output do build

pnpm build
cat dist/assets/index-*.js | head -20
# Código minificado e otimizado!

Mini-Projeto: Continuação

Vamos adicionar monitoramento de HMR ao nosso Dashboard:

Arquivo: src/hmr-monitor.ts

// src/hmr-monitor.ts
// Monitor de atualizações HMR

interface Atualizacao {
  path: string
  timestamp: string
}

const atualizacoes: Atualizacao[] = []

export function registrarAtualizacao(
  path: string
): void {
  atualizacoes.push({
    path,
    timestamp: new Date().toLocaleTimeString('pt-BR')
  })
}

export function getAtualizacoes(): Atualizacao[] {
  return [...atualizacoes]
}

export function getUltimaAtualizacao(): Atualizacao | null {
  return atualizacoes[atualizacoes.length - 1] || null
}

// Configura listener de HMR
if (import.meta.hot) {
  // Quando QUALQUER módulo for atualizado
  import.meta.hot.on(
    'vite:beforeUpdate',
    (payload) => {
      payload.updates.forEach(
        (update: { path: string }) => {
          registrarAtualizacao(update.path)
          console.log(`HMR: ${update.path}`)
        }
      )
    }
  )
}

Atualize o main.ts

// main.ts
import './style.css'
import { setupCounter } from './counter'
import {
  getAtualizacoes,
  getUltimaAtualizacao
} from './hmr-monitor'

const inicioCarregamento: number = performance.now()

function renderApp(): void {
  const atualizacoes = getAtualizacoes()
  const ultima = getUltimaAtualizacao()

  const app = document.querySelector<HTMLDivElement>(
    '#app'
  )
  if (!app) return

  app.innerHTML = `
    <div>
      <a href="https://vitejs.dev" target="_blank">
        <img src="/vite.svg" class="logo"
          alt="Vite logo" />
      </a>
      <h1>Dashboard Vite</h1>

      <div class="card">
        <button id="counter" type="button"></button>
      </div>

      <div class="stats">
        <p id="tempo-carregamento">
          Calculando...
        </p>
        <p id="hmr-stats">
          HMR Updates: ${atualizacoes.length}
          ${ultima
            ? `<br>Último: ${ultima.path} às ${ultima.timestamp}`
            : ''
          }
        </p>
      </div>
    </div>
  `

  const btn = document.querySelector<HTMLButtonElement>(
    '#counter'
  )
  if (btn) setupCounter(btn)

  requestAnimationFrame(() => {
    const fimCarregamento: number = performance.now()
    const tempoTotal: string = (
      fimCarregamento - inicioCarregamento
    ).toFixed(2)
    const el = document.querySelector(
      '#tempo-carregamento'
    )
    if (el) {
      el.textContent = `Carregado em ${tempoTotal}ms`
    }
  })
}

renderApp()

// Aceita HMR e re-renderiza
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    renderApp()
  })
}

Teste o HMR

  1. Rode pnpm dev
  2. Abra o navegador em http://localhost:5173
  3. Edite qualquer arquivo e salve
  4. Observe o contador de HMR aumentar!

Desafio da Aula

Objetivo

Criar um componente que mostra o tempo de cada atualização HMR.

Instruções

  1. Modifique hmr-monitor.ts para também registrar quanto tempo cada HMR levou
  2. Use performance.now() para medir o tempo entre o início e fim do HMR
  3. Exiba a média de tempo de HMR no dashboard

Dica

// Você pode usar estes eventos do HMR:
import.meta.hot.on(
  'vite:beforeUpdate',
  () => { /* antes */ }
)
import.meta.hot.on(
  'vite:afterUpdate',
  () => { /* depois */ }
)

Spec de Verificação

  • O dashboard mostra quantas atualizações HMR ocorreram
  • O dashboard mostra o tempo médio das atualizações
  • Ao editar um arquivo, os números atualizam automaticamente

Solução

Clique para ver a solução
// src/hmr-monitor.ts

interface AtualizacaoComTempo {
  path: string
  timestamp: string
  duracao: string
}

const atualizacoes: AtualizacaoComTempo[] = []
let hmrInicio: number | null = null

export function registrarInicio(): void {
  hmrInicio = performance.now()
}

export function registrarFim(path: string): void {
  if (hmrInicio) {
    const duracao: number = performance.now() - hmrInicio
    atualizacoes.push({
      path,
      timestamp: new Date().toLocaleTimeString('pt-BR'),
      duracao: duracao.toFixed(2)
    })
    hmrInicio = null
  }
}

export function getAtualizacoes(): AtualizacaoComTempo[] {
  return [...atualizacoes]
}

export function getMediaTempo(): string {
  if (atualizacoes.length === 0) return '0'
  const soma: number = atualizacoes.reduce(
    (acc, a) => acc + parseFloat(a.duracao),
    0
  )
  return (soma / atualizacoes.length).toFixed(2)
}

if (import.meta.hot) {
  import.meta.hot.on(
    'vite:beforeUpdate',
    () => {
      registrarInicio()
    }
  )

  import.meta.hot.on(
    'vite:afterUpdate',
    (payload) => {
      payload.updates.forEach(
        (update: { path: string }) => {
          registrarFim(update.path)
          console.log(`HMR: ${update.path}`)
        }
      )
    }
  )
}

Recursos Adicionais


Próxima aula: 2.3 — Criando e Explorando um Projeto Vite