Usar JavaScript vanilla no obliga a escribir código desordenado. El problema aparece cuando se confunde “sin framework” con “sin arquitectura”: selectores repartidos por toda la página, eventos duplicados, estado escondido en el DOM y estilos que solo funcionan por accidente.
Una arquitectura basada en componentes puede existir perfectamente sin React, Vue o Svelte. La diferencia es que tienes que ser más explícito con los contratos: qué recibe cada pieza, qué evento emite, qué estado controla y qué parte del DOM le pertenece.
Puntos clave
- Vanilla JS funciona bien cuando el estado es local, las interacciones son acotadas y el coste de framework no compensa.
- Un componente no es solo un bloque HTML: necesita contrato, inicialización, eventos, accesibilidad y limpieza.
- La arquitectura debe permitir migrar después sin reescribir todo desde cero.
- El rendimiento mejora cuando cada componente carga solo lo que necesita.
- Si aparece mucho estado compartido o rutas complejas, conviene evaluar un framework.
Qué entiendo por componente en este contexto
Un componente vanilla es una unidad de interfaz con responsabilidad concreta. Puede ser una tarjeta interactiva, un buscador, un acordeón, un modal, un filtro, un menú o una tabla editable. Lo importante no es el tamaño, sino el límite.
Un buen componente responde a estas preguntas:
| Pregunta | Ejemplo |
|---|---|
| ¿Qué DOM controla? | Solo el nodo raíz recibido en init() |
| ¿Qué datos necesita? | Props desde data-*, JSON embebido o API |
| ¿Qué eventos emite? | filter:change, modal:open, form:valid |
| ¿Qué estado mantiene? | Abierto/cerrado, valor seleccionado, página actual |
| ¿Cómo se destruye? | Limpieza de listeners, timers y observers |
Estructura de carpetas que escala sin framework
Una estructura simple puede ser suficiente:
src/
components/
accordion/
accordion.js
accordion.css
README.md
lead-filter/
lead-filter.js
lead-filter.css
contract.md
lib/
dom.js
events.js
fetch-json.js
pages/
blog.js
landing.js
El objetivo no es imitar un framework, sino evitar acoplamientos invisibles. Cada componente debería exponer una función de inicialización y, si procede, una función de destrucción:
export function initAccordion(root) {
const buttons = [...root.querySelectorAll("[data-accordion-trigger]")];
function onClick(event) {
const panel = event.currentTarget.nextElementSibling;
const isOpen = event.currentTarget.getAttribute("aria-expanded") === "true";
event.currentTarget.setAttribute("aria-expanded", String(!isOpen));
panel.hidden = isOpen;
}
buttons.forEach((button) => button.addEventListener("click", onClick));
return () => {
buttons.forEach((button) => button.removeEventListener("click", onClick));
};
}
Ese patrón obliga a pensar en ownership. El componente no busca por toda la página, no muta nodos ajenos y no deja listeners colgados.
Contratos antes que convenciones frágiles
Cuando un componente falla en producción, casi siempre el problema está en un contrato implícito. Un diseñador cambia una clase, un editor mueve un bloque, un endpoint devuelve un campo distinto y el componente deja de funcionar.
Para evitarlo, cada componente debería documentar:
- selector raíz;
- atributos
data-*que consume; - eventos que escucha y emite;
- requisitos de accesibilidad;
- dependencias de CSS o tokens;
- errores esperados;
- ejemplo mínimo de HTML.
Ejemplo de contrato:
<section data-component="lead-filter">
<button data-filter="enterprise">Empresas</button>
<button data-filter="agency">Agencias</button>
<div data-results></div>
</section>
import { initLeadFilter } from "./components/lead-filter/lead-filter.js";
document
.querySelectorAll('[data-component="lead-filter"]')
.forEach((root) => initLeadFilter(root));
Estado: local por defecto, compartido solo cuando aporta
Vanilla se complica cuando el estado compartido se improvisa. Antes de crear un store global, conviene preguntar si el estado realmente lo necesita.
| Estado | Dónde viviría | Motivo |
|---|---|---|
| Modal abierto/cerrado | Componente | No afecta al resto |
| Filtro de una lista | Componente o URL | Puede necesitar deep-linking |
| Usuario autenticado | Capa de aplicación | Lo usan varias piezas |
| Carrito o checkout | Store claro o framework | Estado crítico y compartido |
Si un proyecto empieza a necesitar sincronización constante entre componentes, rutas dinámicas y actualizaciones reactivas, quizá la pregunta ya no sea “cómo hacerlo en vanilla”, sino “qué framework reduce riesgo”.
Rendimiento y carga progresiva
Una ventaja real de vanilla es poder cargar poco JavaScript. Pero esa ventaja se pierde si cada página importa un bundle común con interacciones que no usa. En Astro, por ejemplo, la filosofía de islas ayuda precisamente a enviar menos JavaScript al cliente cuando la página puede ser estática.
Un criterio práctico:
- HTML funcional primero.
- CSS sin dependencia de JavaScript siempre que sea posible.
- JavaScript solo para mejorar interacción.
- Importación por página o por componente.
- Medición de peso, INP y errores de consola.
Cuándo migrar a un framework
Migrar tiene sentido cuando el coste de mantener la arquitectura manual supera el coste del framework. Señales típicas:
- muchas piezas dependen del mismo estado;
- hay rutas, permisos y vistas dinámicas complejas;
- el equipo necesita un patrón común de testing;
- hay duplicación de lógica entre componentes;
- las interacciones dependen de datos en tiempo real;
- los cambios de producto rompen piezas no relacionadas.
La migración no tiene por qué ser total. Se puede aislar primero la zona con más complejidad y mantener vanilla en páginas estáticas o componentes simples.
Lecturas relacionadas
- Performance y conversión: cómo leer Core Web Vitals
- Refactorización web urgente: señales, riesgos y plan de acción
- Plugins de WordPress en 2026: cuándo instalar y cuándo desarrollar
Conclusión
Vanilla JS no es una excusa para renunciar a la arquitectura. Al contrario: obliga a ser más consciente de contratos, límites y coste de mantenimiento. Si el proyecto es simple, esa claridad puede ser una ventaja enorme. Si deja de serlo, la misma arquitectura te dará una ruta de migración menos traumática.
Preguntas frecuentes
- ¿Tiene sentido usar componentes sin React o Vue?
- Sí, si el producto tiene interacciones acotadas, poca necesidad de estado global y prioridad en rendimiento. La clave es definir contratos, eventos y límites de responsabilidad.
- ¿Cuándo conviene migrar desde vanilla JS a un framework?
- Conviene migrar cuando aparecen estado compartido complejo, rutas ricas, equipos grandes, reutilización intensa o pruebas que se vuelven difíciles con módulos sueltos.
- ¿Qué debe tener un componente vanilla mantenible?
- Debe tener una API clara, estado local controlado, eventos documentados, accesibilidad, pruebas mínimas y estilos que no dependan de selectores frágiles.