1.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
npm run dev
PRODUÇÃO
  • Rollup Bundler
  • + Plugins
  • + Otimizações
npm run 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.js">
2
GET /src/main.js
Transforma main.js sob demanda
Navegador recebe main.js, encontra: import './app.js'
3
GET /src/app.js
Transforma app.js sob demanda
E assim por diante para cada import...

Código Exemplo: Vendo na Prática

Crie estes arquivos no seu projeto:

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

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

Abra o DevTools do navegadorNetwork e observe:

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

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


Pré-bundling de Dependências

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!

// react usa CommonJS internamente
import React from 'react'
// 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 npm run dev:

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

Vendo o Cache

Após rodar npm run dev, observe a pasta criada:

ls node_modules/.vite/deps/

Você verá arquivos como:

_metadata.json
lodash-es.js
lodash-es.js.map
react.js
react.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)

Como Funciona

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

HMR FLOW

1
Você edita: src/components/Button.js
2
Vite detecta a mudança (chokidar file watcher)
3
Vite envia via WebSocket:
{ type: 'update', updates: [{
  path: '/src/components/Button.js',
  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.js

API do HMR

Você pode interagir com o HMR programaticamente:

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

console.log('Contagem:', contador)

// 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

npm run build

src/
main.js
app.js
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?

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
npm run dev

Você verá:

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

2. Compare dev vs build

# Em desenvolvimento
npm run dev
# Abra DevTools → Network → observe dezenas de arquivos .js

# Para build
npm run build
# Observe a pasta dist/ → poucos arquivos otimizados

3. Inspecione o output do build

npm run 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.js

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

const atualizacoes = []

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

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

export function getUltimaAtualizacao() {
  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 => {
      registrarAtualizacao(update.path)
      console.log(`🔄 HMR: ${update.path}`)
    })
  })
}

Atualize o main.js

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

const inicioCarregamento = performance.now()

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

  document.querySelector('#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>
  `

  setupCounter(document.querySelector('#counter'))

  requestAnimationFrame(() => {
    const fimCarregamento = performance.now()
    const tempoTotal = (fimCarregamento - inicioCarregamento).toFixed(2)
    document.querySelector('#tempo-carregamento').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 npm run 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.js 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.js
const atualizacoes = []
let hmrInicio = null

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

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

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

export function getMediaTempo() {
  if (atualizacoes.length === 0) return 0
  const soma = 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 => {
      registrarFim(update.path)
      console.log(`🔄 HMR: ${update.path}`)
    })
  })
}

📚 Recursos Adicionais


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