Volver al blog

El Regreso de Vanilla JavaScript en 2026: Menos Frameworks, Más Claridad

Hola HaWkers, algo interesante está pasando en el ecosistema JavaScript. Después de años de "guerra de frameworks", un movimiento creciente de desarrolladores está eligiendo conscientemente usar JavaScript puro en nuevos proyectos. No por falta de conocimiento, sino por claridad de propósito.

¿Por qué los desarrolladores experimentados están volviendo a lo básico? ¿Y cuándo Vanilla JS tiene más sentido que un framework?

El Movimiento Vanilla

No es regresión, es evolución.

Por Qué Ahora

Factores que impulsan el cambio:

APIs nativas maduras:

  • Fetch API estable y poderosa
  • Web Components soportados en todos los navegadores
  • ES Modules nativos funcionando
  • CSS Container Queries y :has()
  • Dialog, Popover y otras APIs nativas

Fatiga de frameworks:

  • Nuevo framework cada semana
  • Breaking changes constantes
  • Curva de aprendizaje infinita
  • Dependencias desactualizadas

El rendimiento importa más:

  • Core Web Vitals afectan SEO
  • Usuarios en dispositivos limitados
  • Costo de JavaScript demasiado alto
  • TTI (Time to Interactive) crítico

Qué Cambió en JavaScript Nativo

El lenguaje ha evolucionado mucho.

APIs Modernas

Lo que antes necesitaba biblioteca:

// Antes: jQuery para AJAX
$.ajax({
  url: '/api/users',
  method: 'GET',
  success: function(data) { /* ... */ }
});

// Ahora: Fetch nativo
const response = await fetch('/api/users');
const users = await response.json();

// Antes: Lodash para debounce
import { debounce } from 'lodash';
const debouncedFn = debounce(fn, 300);

// Ahora: Scheduler API (o simple implementación)
const debouncedFn = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

// Antes: Moment.js para fechas
moment('2026-01-15').format('DD/MM/YYYY');

// Ahora: Intl.DateTimeFormat nativo
new Intl.DateTimeFormat('es-ES').format(new Date('2026-01-15'));
// O pronto: Temporal API

Manipulación DOM Moderna

Sin jQuery, con poder:

// Selección de elementos
const button = document.querySelector('.submit-btn');
const items = document.querySelectorAll('.item');

// Event delegation eficiente
document.querySelector('.list').addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleItemClick(e.target);
  }
});

// Manipulación de clases
element.classList.add('active', 'visible');
element.classList.toggle('open');
element.classList.replace('old', 'new');

// Data attributes
element.dataset.userId = '123';
const id = element.dataset.userId;

// Template literals para HTML
const html = `
  <div class="card">
    <h2>${title}</h2>
    <p>${description}</p>
  </div>
`;
container.insertAdjacentHTML('beforeend', html);

// Intersection Observer (lazy loading nativo)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

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

Web Components Nativos

Componentes sin framework.

Custom Elements

Creando componentes reutilizables:

// Definición del componente
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  static get observedAttributes() {
    return ['name', 'avatar', 'role'];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    const name = this.getAttribute('name') || 'Desconocido';
    const avatar = this.getAttribute('avatar') || '/default.png';
    const role = this.getAttribute('role') || 'Usuario';

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 1rem;
          border-radius: 8px;
          background: var(--card-bg, #fff);
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .card {
          display: flex;
          align-items: center;
          gap: 1rem;
        }
        img {
          width: 48px;
          height: 48px;
          border-radius: 50%;
        }
        h3 { margin: 0; }
        span { color: #666; font-size: 0.9rem; }
      </style>
      <div class="card">
        <img src="${avatar}" alt="${name}">
        <div>
          <h3>${name}</h3>
          <span>${role}</span>
        </div>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);
<!-- Uso en HTML -->
<user-card
  name="Juan García"
  avatar="/avatars/juan.jpg"
  role="Desarrollador Senior">
</user-card>

Slots y Composición

Componentes flexibles:

class Modal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: none;
          position: fixed;
          inset: 0;
          background: rgba(0,0,0,0.5);
          place-items: center;
        }
        :host([open]) {
          display: grid;
        }
        .modal {
          background: white;
          padding: 2rem;
          border-radius: 8px;
          max-width: 500px;
          width: 90%;
        }
        ::slotted([slot="header"]) {
          margin-top: 0;
        }
      </style>
      <div class="modal">
        <slot name="header"></slot>
        <slot></slot>
        <slot name="footer"></slot>
      </div>
    `;

    // Cerrar al hacer clic fuera
    this.addEventListener('click', (e) => {
      if (e.target === this) {
        this.close();
      }
    });
  }

  open() {
    this.setAttribute('open', '');
  }

  close() {
    this.removeAttribute('open');
    this.dispatchEvent(new CustomEvent('modal-close'));
  }
}

customElements.define('app-modal', Modal);

Estado Sin Framework

Gestionando estado con JavaScript puro.

Store Simple

Patrón pub/sub minimalista:

// store.js - Estado global simple
function createStore(initialState = {}) {
  let state = initialState;
  const listeners = new Set();

  return {
    getState() {
      return state;
    },

    setState(newState) {
      state = typeof newState === 'function'
        ? newState(state)
        : { ...state, ...newState };

      listeners.forEach(listener => listener(state));
    },

    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
}

// Uso
const store = createStore({
  user: null,
  items: [],
  loading: false
});

// Componente se suscribe
const unsubscribe = store.subscribe((state) => {
  renderUserInfo(state.user);
  renderItems(state.items);
});

// Actualizar estado
store.setState({ loading: true });
const items = await fetchItems();
store.setState({ items, loading: false });

Proxy Reactivo

Reactividad sin biblioteca:

// Proxy para estado reactivo
function reactive(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];
      if (typeof value === 'object' && value !== null) {
        return reactive(value, onChange);
      }
      return value;
    }
  });
}

// Uso
const state = reactive({
  count: 0,
  user: { name: 'Juan' }
}, (prop, newValue, oldValue) => {
  console.log(`${prop} cambió: ${oldValue}${newValue}`);
  updateUI();
});

state.count++; // Dispara onChange
state.user.name = 'María'; // También funciona en objetos anidados

Cuándo Usar Vanilla JS

Eligiendo conscientemente.

Casos Ideales

Donde Vanilla JS brilla:

Landing pages:

  • Interactividad limitada
  • Rendimiento crítico
  • SEO importante
  • Contenido mayormente estático

Widgets embeddable:

  • Sin conflicto con el host
  • Bundle mínimo
  • Independencia total

Bibliotecas y herramientas:

  • Sin asumir el framework del usuario
  • Máxima compatibilidad
  • Menor footprint

Sitios de contenido:

  • Blogs, documentación
  • Portafolios
  • Sitios institucionales

Cuándo el Framework Aún Tiene Sentido

No es todo o nada:

SPAs complejas:

  • Muchos estados interdependientes
  • Enrutamiento client-side extenso
  • Equipo grande trabajando junto

Aplicaciones en tiempo real:

  • Dashboards con muchas actualizaciones
  • Chat y colaboración
  • Interfaces altamente dinámicas

Productividad del equipo:

  • Equipo ya conoce el framework
  • Ecosistema maduro necesario
  • Patrones establecidos

Herramientas Para Vanilla JS

Desarrollo moderno sin framework.

Build Tools

Configuración mínima:

// vite.config.js para proyecto vanilla
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        about: 'about.html'
      }
    }
  }
});

Comparativo de Bundle Size

Números que importan:

Enfoque Bundle Size First Paint
Vanilla JS ~5 KB ~50ms
Preact ~10 KB ~80ms
Vue 3 ~35 KB ~120ms
React ~45 KB ~150ms
Angular ~100 KB ~250ms

💡 Nota: Estos números son aproximados y varían según la aplicación. El punto es que Vanilla JS tiene cero overhead de framework.

El regreso de Vanilla JavaScript no se trata de rechazar frameworks - se trata de elegir la herramienta correcta para cada trabajo. Con APIs nativas maduras, Web Components estables y un lenguaje más poderoso que nunca, JavaScript puro es una elección legítima y a menudo superior.

Si quieres dominar los fundamentos que nunca cambian, te recomiendo ver otro artículo: ES2026 y Temporal API donde descubrirás las nuevas features nativas del lenguaje.

¡Vamos con todo! 🦅

💻 Domina JavaScript de Verdad

El conocimiento que adquiriste en este artículo es solo el comienzo. Dominar JavaScript puro es la base que todo desarrollador necesita.

Invierte en Tu Futuro

He preparado material completo para que domines JavaScript:

Formas de pago:

  • 1x de $4.90 sin intereses
  • o $4.90 al contado

📖 Ver Contenido Completo

Comentarios (0)

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

Añadir comentarios