7 minpwajavascriptperformance

PWA com HTML, CSS e JS puro: por que evitei framework

A escolha de não usar React no Prev Calc não foi nostalgia. Foi cálculo: bundle size, tempo de hidratação, complexidade de build e a régua de manutenção a longo prazo.


Quando contei que o Prev Calc era HTML, CSS e JavaScript puro, recebi mais perguntas sobre essa escolha do que sobre o cálculo de previdência em si.

"Por que não React?" "Por que não Vue?" "Por que não Astro, pelo menos?"

A resposta curta: porque framework não me dava nada que eu precisasse, e cobrava coisas que eu não queria pagar. A resposta longa é esse post.

O que um framework te dá

Antes de defender minha escolha, é honesto reconhecer o que React (ou similar) entrega:

  1. Componentização — reutilização e composição de UI
  2. Estado reativouseState, useEffect automatizam atualizações de DOM
  3. Roteamento e navegação — React Router, Next.js routing
  4. Ecossistema — bibliotecas para tudo (forms, dates, charts, auth)
  5. Hot reload e dev experience — ferramental moderno
  6. TypeScript de primeira classe — tipagem fluida em componentes

Para um app com 50+ telas, formulários complexos, fluxos de auth, isso justifica o custo. O Prev Calc tem 5 telas.

O custo que poucos contam

O custo de um framework não é só o bundle JavaScript. É:

  • Build pipeline. Webpack, Vite ou Turbopack. Lock file. Update de dependências. Patch de segurança. CVEs no pipeline de build.
  • Hidratação. O HTML chega rápido, mas o app só fica interativo depois que o JavaScript executa.
  • Versionamento de runtime. React 16 → 17 → 18 → 19. Server Components mudou tudo. Cada major rompe alguma coisa.
  • Stack de dependências. React tem ~50KB. Mas você raramente usa "só React" — você usa React + React DOM + React Router + uma lib de form + uma lib de date + um charting + uma de fetch...
  • Hosting que entende isso. SSR vs SSG vs ISR. Edge runtime. Você precisa de uma plataforma como Vercel ou cuidar do servidor.

Para um app de 5 telas com cálculo síncrono no client, isso é overkill com taxa de manutenção mensal.

A régua que apliquei

Antes de cravar a stack, fiz quatro perguntas:

1. O app precisa de navegação SPA real? Não. As 5 telas são abas de uma calculadora. Mostro/escondo <section> com display. O Prev Calc não precisa de URL por tela.

2. Tenho componentes reutilizáveis suficientes para justificar componentização? Não. São inputs, sliders e o gráfico. Cada elemento tem layout próprio, comportamento próprio.

3. O estado é complexo? Não. É um único objeto de "configuração de simulação" que recalcula tudo a cada mudança. addEventListener('input', recalcular) resolve.

4. Quero que esse app rode em 2030? Sim. E HTML, CSS e JS vanilla são as únicas tecnologias web que eu tenho razoável certeza que vão estar funcionais sem update em cinco anos.

Resposta nesse caso específico: o framework não pagaria, e teria risco de virar dívida.

A stack que sobrou

prev-calc/
├── index.html        // 5 <section> com inputs e o gráfico
├── style.css         // ~400 linhas, vanilla CSS com custom properties
├── calc.js           // o motor de simulação (funções puras)
├── ui.js             // event handlers, atualização do DOM
├── chart.js          // wrapper sobre Chart.js para renderizar gráficos
├── sw.js             // Service Worker para offline
├── manifest.json     // PWA manifest
└── assets/
    └── icons/        // PNGs em vários tamanhos

Sem package.json. Sem node_modules. Sem build.

O deploy é: git push para o repositório, e o Vercel serve os arquivos estáticos. Total: ~150KB de assets, ~50KB de JavaScript não-comprimido. Carrega em 200ms até em 3G.

Estado reativo sem framework

A pergunta mais frequente: "como você gerencia estado sem React?"

Resposta:

// Estado da simulação
const state = {
  idade: 35,
  contribMensal: 1000,
  taxaReal: 5,
  inflacao: 4,
  // ...
};

// Função pura: recebe state, retorna resultado
function simular(state) {
  // ...cálculos...
  return { patrimonioFinal, rendaMensal, fluxos };
}

// Renderização: recebe resultado, atualiza DOM
function renderizar(resultado) {
  document.getElementById("patrimonio").textContent = formatar(resultado.patrimonioFinal);
  document.getElementById("renda").textContent = formatar(resultado.rendaMensal);
  atualizarGrafico(resultado.fluxos);
}

// Loop reativo
document.querySelectorAll("input").forEach((input) => {
  input.addEventListener("input", (e) => {
    state[e.target.name] = parseFloat(e.target.value);
    const resultado = simular(state);
    renderizar(resultado);
  });
});

São 30 linhas. Isso replica o que useState + useEffect fazem em um framework.

A diferença é que aqui o state é uma variável, não um hook. Você lê e escreve diretamente. Não tem cerimônia, não tem regras de "não pode mutar", não tem useCallback para memoizar. É só JavaScript.

Service Worker para offline

A parte que importa para um PWA real: funcionar sem internet.

// sw.js
const CACHE_NAME = "prev-calc-v1";
const ASSETS = [
  "/",
  "/index.html",
  "/style.css",
  "/calc.js",
  "/ui.js",
  "/chart.js",
  "/manifest.json",
  // ...
];

self.addEventListener("install", (e) => {
  e.waitUntil(caches.open(CACHE_NAME).then((c) => c.addAll(ASSETS)));
});

self.addEventListener("fetch", (e) => {
  e.respondWith(
    caches.match(e.request).then((res) => res || fetch(e.request)),
  );
});

50 linhas, cache-first, funciona. O usuário instala o app, e ele continua respondendo offline. Zero servidor, zero infraestrutura.

O que dói nessa stack

Não é tudo flores. Coisas que dói:

1. Sem TypeScript. Para esse projeto, eu tolerei. JSDoc + IDE com inferência cobre 60% dos benefícios. Mas para qualquer coisa maior, perderia TypeScript me afetaria.

2. Manipulação manual de DOM. Em projetos pequenos, getElementById + textContent é OK. Em projetos com listas dinâmicas longas, vira um inferno. Aí, sim, framework paga.

3. Sem componentes reutilizáveis automáticos. Se eu quiser reaproveitar um "input com label e tooltip" em 20 lugares, preciso fazer um helper. Em React eu faria um <Field /> em 5 minutos.

4. Falta de convenções. Em projeto solo, isso é só estilo pessoal. Em time, vira anarquia. Cada dev tem sua maneira de organizar addEventListener.

A pergunta que importa

A escolha de framework não é estética. É quanto da complexidade da sua aplicação justifica importar a complexidade da ferramenta.

Para o Prev Calc, a aplicação é simples (5 telas, cálculo síncrono, sem auth, sem servidor). Importar React e o ecossistema dele triplicaria a complexidade do projeto sem entregar valor.

Para o BeachTennis Manager — outro projeto meu, com 30+ telas, sync na nuvem, formulários complexos, autenticação — Next.js é a escolha que faz mais sentido.

A régua não é "framework é bom" ou "framework é ruim". É: a complexidade do problema justifica a complexidade da ferramenta?

Se sim, framework. Se não, vanilla.

E em mais projetos do que costuma se assumir por padrão, a resposta honesta é "não".