Voltar para o Blog

Vanilla JavaScript em 2026: O Retorno do JS Puro e Por Que Menos é Mais

Olá HaWkers, uma tendência interessante está ganhando força no desenvolvimento web em 2026. O Vanilla JavaScript, antes descartado como "muito básico", está ressurgindo como uma escolha inteligente para desenvolvedores que buscam performance e simplicidade.

Vamos explorar por que o JS puro está voltando e quando faz sentido abandonar os frameworks.

O Que Mudou

O cenário mudou significativamente nos últimos anos:

Fatores da mudança:

  • APIs nativas do navegador evoluíram drasticamente
  • Performance se tornou crítica (Core Web Vitals, SEO)
  • Fadiga de frameworks atingiu o pico
  • Custos de manutenção de dependências explodiram
  • Complexidade desnecessária em projetos simples

💡 Contexto: Em 2020, usar vanilla JS era visto como amadorismo. Em 2026, é considerado uma decisão técnica madura para muitos casos.

O Poder do JavaScript Moderno

O JavaScript nativo de 2026 é incrivelmente poderoso:

APIs Nativas Avançadas

// APIs modernas que eliminam necessidade de bibliotecas

// 1. Fetch API - Substituiu bibliotecas HTTP
const response = await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'HaWker' }),
  signal: AbortSignal.timeout(5000), // Timeout nativo!
});

// 2. Web Components - Componentes sem framework
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .card { padding: 1rem; border: 1px solid #ddd; }
      </style>
      <div class="card">
        <slot name="name"></slot>
        <slot name="email"></slot>
      </div>
    `;
  }
}
customElements.define('user-card', UserCard);

// 3. Intersection Observer - Lazy loading nativo
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.src = entry.target.dataset.src;
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

Manipulação de DOM Moderna

// DOM manipulation sem jQuery ou similar

// Seletores poderosos
const buttons = document.querySelectorAll('.btn[data-action]');
const form = document.querySelector('#signup-form');

// Event delegation eficiente
document.body.addEventListener('click', (e) => {
  const button = e.target.closest('[data-action]');
  if (!button) return;

  const action = button.dataset.action;
  handlers[action]?.(e);
});

// Templates nativos
const template = document.getElementById('item-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.title').textContent = 'Novo Item';
document.getElementById('list').appendChild(clone);

// ClassList API
element.classList.add('active', 'visible');
element.classList.remove('hidden');
element.classList.toggle('expanded');
element.classList.replace('old-class', 'new-class');

Estado Sem Framework

Gerenciar estado sem React/Vue é mais simples do que você imagina:

Padrão Observer Simples

// Sistema de estado reativo minimalista

class Store {
  #state = {};
  #listeners = new Map();

  constructor(initialState = {}) {
    this.#state = initialState;
  }

  getState() {
    return structuredClone(this.#state);
  }

  setState(updates) {
    const prevState = this.#state;
    this.#state = { ...this.#state, ...updates };

    // Notifica apenas listeners afetados
    for (const [key, callbacks] of this.#listeners) {
      if (key in updates && prevState[key] !== updates[key]) {
        callbacks.forEach(cb => cb(updates[key], prevState[key]));
      }
    }
  }

  subscribe(key, callback) {
    if (!this.#listeners.has(key)) {
      this.#listeners.set(key, new Set());
    }
    this.#listeners.get(key).add(callback);

    // Retorna função de unsubscribe
    return () => this.#listeners.get(key).delete(callback);
  }
}

// Uso
const store = new Store({ user: null, cart: [], theme: 'light' });

// Componente reage a mudanças
store.subscribe('cart', (newCart) => {
  document.getElementById('cart-count').textContent = newCart.length;
});

store.subscribe('theme', (theme) => {
  document.body.classList.toggle('dark', theme === 'dark');
});

// Atualizar estado
store.setState({ cart: [...store.getState().cart, newItem] });

Proxy para Reatividade

// Reatividade automática com Proxy

function createReactive(target, onChange) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const oldValue = obj[prop];
      obj[prop] = value;

      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }

      return true;
    },

    get(obj, prop) {
      const value = obj[prop];

      // Recursivo para objetos aninhados
      if (value && typeof value === 'object') {
        return createReactive(value, onChange);
      }

      return value;
    }
  });
}

// Uso
const state = createReactive(
  { count: 0, user: { name: 'HaWker' } },
  (prop, newVal, oldVal) => {
    console.log(`${prop} mudou de ${oldVal} para ${newVal}`);
    updateUI();
  }
);

state.count++; // Dispara automaticamente
state.user.name = 'Dev'; // Também reativo!

Roteamento Sem Biblioteca

SPAs são possíveis com APIs nativas:

// Router minimalista com History API

class Router {
  #routes = new Map();
  #notFound = () => {};

  constructor() {
    window.addEventListener('popstate', () => this.#navigate());

    // Intercepta links
    document.addEventListener('click', (e) => {
      const link = e.target.closest('a[data-link]');
      if (!link) return;

      e.preventDefault();
      this.push(link.getAttribute('href'));
    });
  }

  route(path, handler) {
    this.#routes.set(path, handler);
    return this;
  }

  notFound(handler) {
    this.#notFound = handler;
    return this;
  }

  push(path) {
    history.pushState(null, '', path);
    this.#navigate();
  }

  #navigate() {
    const path = location.pathname;

    // Tenta match exato
    if (this.#routes.has(path)) {
      this.#routes.get(path)();
      return;
    }

    // Tenta match com parâmetros
    for (const [routePath, handler] of this.#routes) {
      const params = this.#matchRoute(routePath, path);
      if (params) {
        handler(params);
        return;
      }
    }

    this.#notFound();
  }

  #matchRoute(routePath, actualPath) {
    const routeParts = routePath.split('/');
    const actualParts = actualPath.split('/');

    if (routeParts.length !== actualParts.length) return null;

    const params = {};

    for (let i = 0; i < routeParts.length; i++) {
      if (routeParts[i].startsWith(':')) {
        params[routeParts[i].slice(1)] = actualParts[i];
      } else if (routeParts[i] !== actualParts[i]) {
        return null;
      }
    }

    return params;
  }

  start() {
    this.#navigate();
    return this;
  }
}

// Uso
const router = new Router()
  .route('/', () => renderHome())
  .route('/blog', () => renderBlog())
  .route('/blog/:slug', ({ slug }) => renderPost(slug))
  .route('/user/:id/posts', ({ id }) => renderUserPosts(id))
  .notFound(() => render404())
  .start();

Comparação de Bundle Size

O impacto no tamanho do bundle é dramático:

Abordagem Bundle Size Tempo de Parse
React + Router + State ~150KB gzip ~200ms
Vue 3 + Router + Pinia ~80KB gzip ~120ms
Vanilla JS (equivalente) ~5KB gzip ~10ms

Impacto Real em Performance

// Medindo impacto real

const performanceComparison = {
  // Tempo para First Contentful Paint
  fcp: {
    react: '1.2s - 2.5s',
    vue: '0.8s - 1.8s',
    vanilla: '0.3s - 0.6s'
  },

  // Time to Interactive
  tti: {
    react: '2.5s - 4.0s',
    vue: '1.5s - 3.0s',
    vanilla: '0.5s - 1.0s'
  },

  // JavaScript execution time
  jsExecution: {
    react: '150ms - 400ms',
    vue: '80ms - 200ms',
    vanilla: '10ms - 50ms'
  },

  // Memory footprint
  memory: {
    react: '15MB - 30MB',
    vue: '10MB - 20MB',
    vanilla: '3MB - 8MB'
  }
};

Quando Usar Vanilla JS

Casos Ideais

// Projetos onde vanilla JS brilha

const idealCases = {
  // Landing pages e sites estáticos
  staticSites: {
    reason: 'Não precisa de reatividade complexa',
    benefit: 'Performance máxima, SEO melhor',
    examples: ['Portfolio', 'Institucional', 'Landing page']
  },

  // Widgets e componentes isolados
  widgets: {
    reason: 'Componente único não justifica framework',
    benefit: 'Bundle mínimo, fácil integração',
    examples: ['Chat widget', 'Formulário embed', 'Player customizado']
  },

  // Bibliotecas e plugins
  libraries: {
    reason: 'Não deve forçar dependências nos usuários',
    benefit: 'Framework-agnostic, menor footprint',
    examples: ['SDK', 'Analytics', 'UI components']
  },

  // Projetos com requisitos de performance
  performance: {
    reason: 'Cada kilobyte importa',
    benefit: 'Core Web Vitals otimizados',
    examples: ['E-commerce', 'News sites', 'PWAs']
  },

  // Aplicações com vida longa
  longevity: {
    reason: 'Menos dependências = menos breaking changes',
    benefit: 'Manutenção simplificada por anos',
    examples: ['Sistemas internos', 'Tools enterprise']
  }
};

Casos Onde Framework Ainda Faz Sentido

// Quando frameworks são justificados

const frameworkCases = {
  // Apps complexos com muito estado
  complexState: {
    reason: 'Estado compartilhado entre dezenas de componentes',
    recommendation: 'React, Vue, Svelte'
  },

  // Equipes grandes
  largeTeams: {
    reason: 'Convenções e estrutura ajudam coordenação',
    recommendation: 'Angular, Next.js'
  },

  // Ecossistema necessário
  ecosystem: {
    reason: 'Precisa de bibliotecas específicas do framework',
    recommendation: 'Escolha pelo ecossistema'
  },

  // Prototipação rápida
  prototyping: {
    reason: 'Velocidade de desenvolvimento priorizada',
    recommendation: 'Vue, Svelte'
  }
};

Ferramentas para Vanilla JS em 2026

Build Tools Minimalistas

// Configuração moderna para vanilla JS

// esbuild - Build ultrarrápido
// esbuild.config.mjs
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/main.js'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ['es2022'],
  outfile: 'dist/bundle.js',
  // Sem transpilação desnecessária para browsers modernos
  format: 'esm',
});

// Vite para desenvolvimento (sem framework)
// vite.config.js
export default {
  build: {
    target: 'esnext',
    minify: 'esbuild',
    rollupOptions: {
      input: 'src/main.js',
    },
  },
  // Hot reload funciona com vanilla JS!
};

Testing Sem Jest

// Testes nativos com Node.js test runner

import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert';

import { Store } from './store.js';

describe('Store', () => {
  let store;

  beforeEach(() => {
    store = new Store({ count: 0 });
  });

  test('deve inicializar com estado', () => {
    assert.deepStrictEqual(store.getState(), { count: 0 });
  });

  test('deve atualizar estado', () => {
    store.setState({ count: 5 });
    assert.strictEqual(store.getState().count, 5);
  });

  test('deve notificar subscribers', (t) => {
    const callback = t.mock.fn();
    store.subscribe('count', callback);

    store.setState({ count: 10 });

    assert.strictEqual(callback.mock.calls.length, 1);
    assert.deepStrictEqual(callback.mock.calls[0].arguments, [10, 0]);
  });
});

// Executar: node --test

Migração Gradual

Se você quer experimentar, comece gradualmente:

Estratégia de Migração

// Abordagem incremental

const migrationStrategy = {
  phase1: {
    action: 'Auditar dependências',
    tasks: [
      'Listar todas as bibliotecas usadas',
      'Identificar o que pode ser nativo',
      'Calcular economia potencial'
    ]
  },

  phase2: {
    action: 'Substituir utilitários',
    tasks: [
      'Remover Lodash (usar métodos nativos)',
      'Remover Axios (usar Fetch)',
      'Remover Moment (usar Intl, Temporal)'
    ]
  },

  phase3: {
    action: 'Isolar componentes',
    tasks: [
      'Criar Web Components para UI',
      'Mover lógica para módulos ES',
      'Reduzir acoplamento com framework'
    ]
  },

  phase4: {
    action: 'Avaliar framework',
    tasks: [
      'Medir complexidade real do estado',
      'Considerar se framework é necessário',
      'Migrar se benefício for claro'
    ]
  }
};

Exemplo: Substituindo Lodash

// Antes: Lodash (70KB)
import _ from 'lodash';
const unique = _.uniq(array);
const grouped = _.groupBy(items, 'category');
const debounced = _.debounce(fn, 300);

// Depois: Nativo (0KB)
const unique = [...new Set(array)];

const grouped = Object.groupBy(items, item => item.category);
// Ou para browsers mais antigos:
const grouped = items.reduce((acc, item) => {
  (acc[item.category] ??= []).push(item);
  return acc;
}, {});

const debounced = (fn, delay) => {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
};

O Futuro

Tendências para 2026-2027

O que esperar:

  1. Mais APIs nativas no browser
  2. Web Components mais adotados
  3. Frameworks menores e mais focados
  4. "Islands Architecture" mainstream
  5. Compilação ahead-of-time dominante

Para Desenvolvedores

Recomendações:

  • Aprenda JavaScript profundamente, não só frameworks
  • Entenda as APIs nativas do browser
  • Avalie necessidades reais antes de adicionar dependências
  • Performance deve ser consideração de design, não otimização posterior

Conclusão

O retorno do Vanilla JavaScript em 2026 não significa que frameworks morreram. Significa que temos mais opções e maturidade para escolher a ferramenta certa para cada trabalho.

Para projetos simples, sites estáticos, widgets e situações onde performance é crítica, vanilla JS é frequentemente a melhor escolha. Para aplicações complexas com muito estado compartilhado e equipes grandes, frameworks continuam tendo seu lugar.

O importante é fazer escolhas conscientes em vez de seguir cegamente tendências.

Se você quer entender mais sobre tendências de desenvolvimento, recomendo que dê uma olhada em outro artigo: TypeScript É o Padrão em 2026 onde você vai descobrir como o TypeScript dominou o ecossistema.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário