Vanilla JavaScript en 2026: El Retorno del JS Puro y Por Qué Menos es Más
Hola HaWkers, una tendencia interesante está ganando fuerza en el desarrollo web en 2026. Vanilla JavaScript, antes descartado como "muy básico", está resurgiendo como una elección inteligente para desarrolladores que buscan performance y simplicidad.
Vamos a explorar por qué el JS puro está volviendo y cuándo tiene sentido abandonar los frameworks.
Qué Cambió
El escenario ha cambiado significativamente en los últimos años:
Factores del cambio:
- Las APIs nativas del navegador evolucionaron drásticamente
- La performance se volvió crítica (Core Web Vitals, SEO)
- La fatiga de frameworks alcanzó su pico
- Los costos de mantenimiento de dependencias explotaron
- Complejidad innecesaria en proyectos simples
💡 Contexto: En 2020, usar vanilla JS era visto como amateurismo. En 2026, es considerado una decisión técnica madura para muchos casos.
El Poder del JavaScript Moderno
El JavaScript nativo de 2026 es increíblemente poderoso:
APIs Nativas Avanzadas
// APIs modernas que eliminan la necesidad de bibliotecas
// 1. Fetch API - Reemplazó 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 sin 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);
});Manipulación de DOM Moderna
// Manipulación de DOM sin jQuery o similar
// Selectores 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 = 'Nuevo 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 Sin Framework
Gestionar estado sin React/Vue es más simple de lo que imaginas:
Patrón Observer Simple
// Sistema de estado reactivo 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 solo listeners afectados
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 función de unsubscribe
return () => this.#listeners.get(key).delete(callback);
}
}
// Uso
const store = new Store({ user: null, cart: [], theme: 'light' });
// Componente reacciona a cambios
store.subscribe('cart', (newCart) => {
document.getElementById('cart-count').textContent = newCart.length;
});
store.subscribe('theme', (theme) => {
document.body.classList.toggle('dark', theme === 'dark');
});
// Actualizar estado
store.setState({ cart: [...store.getState().cart, newItem] });Proxy para Reactividad
// Reactividad automática con 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 anidados
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} cambió de ${oldVal} a ${newVal}`);
updateUI();
}
);
state.count++; // Dispara automáticamente
state.user.name = 'Dev'; // ¡También reactivo!
Enrutamiento Sin Biblioteca
SPAs son posibles con APIs nativas:
// Router minimalista con 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;
// Intenta match exacto
if (this.#routes.has(path)) {
this.#routes.get(path)();
return;
}
// Intenta match con 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();
Comparación de Tamaño de Bundle
El impacto en el tamaño del bundle es dramático:
| Enfoque | Tamaño Bundle | Tiempo de Parse |
|---|---|---|
| React + Router + State | ~150KB gzip | ~200ms |
| Vue 3 + Router + Pinia | ~80KB gzip | ~120ms |
| Vanilla JS (equivalente) | ~5KB gzip | ~10ms |
Impacto Real en Performance
// Midiendo impacto real
const performanceComparison = {
// Tiempo 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'
}
};
Cuándo Usar Vanilla JS
Casos Ideales
// Proyectos donde vanilla JS brilla
const idealCases = {
// Landing pages y sitios estáticos
staticSites: {
reason: 'No necesita reactividad compleja',
benefit: 'Performance máxima, mejor SEO',
examples: ['Portfolio', 'Institucional', 'Landing page']
},
// Widgets y componentes aislados
widgets: {
reason: 'Componente único no justifica framework',
benefit: 'Bundle mínimo, fácil integración',
examples: ['Chat widget', 'Formulario embed', 'Player customizado']
},
// Bibliotecas y plugins
libraries: {
reason: 'No debe forzar dependencias en los usuarios',
benefit: 'Framework-agnostic, menor footprint',
examples: ['SDK', 'Analytics', 'UI components']
},
// Proyectos con requisitos de performance
performance: {
reason: 'Cada kilobyte importa',
benefit: 'Core Web Vitals optimizados',
examples: ['E-commerce', 'News sites', 'PWAs']
},
// Aplicaciones con vida larga
longevity: {
reason: 'Menos dependencias = menos breaking changes',
benefit: 'Mantenimiento simplificado por años',
examples: ['Sistemas internos', 'Tools enterprise']
}
};Casos Donde Framework Todavía Tiene Sentido
// Cuando frameworks son justificados
const frameworkCases = {
// Apps complejas con mucho estado
complexState: {
reason: 'Estado compartido entre docenas de componentes',
recommendation: 'React, Vue, Svelte'
},
// Equipos grandes
largeTeams: {
reason: 'Convenciones y estructura ayudan coordinación',
recommendation: 'Angular, Next.js'
},
// Ecosistema necesario
ecosystem: {
reason: 'Necesita bibliotecas específicas del framework',
recommendation: 'Elige por ecosistema'
},
// Prototipado rápido
prototyping: {
reason: 'Velocidad de desarrollo priorizada',
recommendation: 'Vue, Svelte'
}
};
Herramientas para Vanilla JS en 2026
Build Tools Minimalistas
// Configuración 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',
// Sin transpilación innecesaria para browsers modernos
format: 'esm',
});
// Vite para desarrollo (sin framework)
// vite.config.js
export default {
build: {
target: 'esnext',
minify: 'esbuild',
rollupOptions: {
input: 'src/main.js',
},
},
// ¡Hot reload funciona con vanilla JS!
};Testing Sin Jest
// Tests nativos con 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('debe inicializar con estado', () => {
assert.deepStrictEqual(store.getState(), { count: 0 });
});
test('debe actualizar estado', () => {
store.setState({ count: 5 });
assert.strictEqual(store.getState().count, 5);
});
test('debe 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]);
});
});
// Ejecutar: node --test
Migración Gradual
Si quieres experimentar, empieza gradualmente:
Estrategia de Migración
// Enfoque incremental
const migrationStrategy = {
phase1: {
action: 'Auditar dependencias',
tasks: [
'Listar todas las bibliotecas usadas',
'Identificar lo que puede ser nativo',
'Calcular ahorro potencial'
]
},
phase2: {
action: 'Sustituir utilitarios',
tasks: [
'Remover Lodash (usar métodos nativos)',
'Remover Axios (usar Fetch)',
'Remover Moment (usar Intl, Temporal)'
]
},
phase3: {
action: 'Aislar componentes',
tasks: [
'Crear Web Components para UI',
'Mover lógica a módulos ES',
'Reducir acoplamiento con framework'
]
},
phase4: {
action: 'Evaluar framework',
tasks: [
'Medir complejidad real del estado',
'Considerar si framework es necesario',
'Migrar si beneficio es claro'
]
}
};Ejemplo: Sustituyendo Lodash
// Antes: Lodash (70KB)
import _ from 'lodash';
const unique = _.uniq(array);
const grouped = _.groupBy(items, 'category');
const debounced = _.debounce(fn, 300);
// Después: Nativo (0KB)
const unique = [...new Set(array)];
const grouped = Object.groupBy(items, item => item.category);
// O para browsers más antiguos:
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);
};
};
El Futuro
Tendencias para 2026-2027
Qué esperar:
- Más APIs nativas en el browser
- Web Components más adoptados
- Frameworks menores y más enfocados
- "Islands Architecture" mainstream
- Compilación ahead-of-time dominante
Para Desarrolladores
Recomendaciones:
- Aprende JavaScript profundamente, no solo frameworks
- Entiende las APIs nativas del browser
- Evalúa necesidades reales antes de agregar dependencias
- Performance debe ser consideración de diseño, no optimización posterior
Conclusión
El retorno de Vanilla JavaScript en 2026 no significa que los frameworks murieron. Significa que tenemos más opciones y madurez para elegir la herramienta correcta para cada trabajo.
Para proyectos simples, sitios estáticos, widgets y situaciones donde la performance es crítica, vanilla JS es frecuentemente la mejor elección. Para aplicaciones complejas con mucho estado compartido y equipos grandes, los frameworks continúan teniendo su lugar.
Lo importante es hacer elecciones conscientes en lugar de seguir ciegamente tendencias.
Si quieres entender más sobre tendencias de desarrollo, te recomiendo que eches un vistazo a otro artículo: TypeScript Es el Estándar en 2026 donde descubrirás cómo TypeScript dominó el ecosistema.

