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:
- Mais APIs nativas no browser
- Web Components mais adotados
- Frameworks menores e mais focados
- "Islands Architecture" mainstream
- 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.

