Volver al blog

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:

  1. Más APIs nativas en el browser
  2. Web Components más adoptados
  3. Frameworks menores y más enfocados
  4. "Islands Architecture" mainstream
  5. 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.

Vamos con todo! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios