Usar JavaScript vanilla no obliga a escriure codi desordenat. El problema apareix quan es confon “sense framework” amb “sense arquitectura”: selectors repartits per tota la pàgina, esdeveniments duplicats, estat amagat al DOM i estils que només funcionen per accident.
Una arquitectura basada en components pot existir perfectament sense React, Vue o Svelte. La diferència és que has de ser més explícit amb els contractes: què rep cada peça, quin esdeveniment emet, quin estat controla i quina part del DOM li pertany.
Punts clau
- Vanilla JS funciona bé quan l’estat és local, les interaccions són acotades i el cost de framework no compensa.
- Un component no és només un bloc HTML: necessita contracte, inicialització, esdeveniments, accessibilitat i neteja.
- L’arquitectura ha de permetre migrar després sense reescriure-ho tot des de zero.
- El rendiment millora quan cada component carrega només el que necessita.
- Si apareix molt estat compartit o rutes complexes, convé avaluar un framework.
Què entenc per component en aquest context
Un component vanilla és una unitat d’interfície amb responsabilitat concreta. Pot ser una targeta interactiva, un cercador, un acordió, un modal, un filtre, un menú o una taula editable. L’important no és la mida, sinó el límit.
Un bon component respon a aquestes preguntes:
| Pregunta | Exemple |
|---|---|
| Quin DOM controla? | Només el node arrel rebut a init() |
| Quines dades necessita? | Props des de data-*, JSON incrustat o API |
| Quins esdeveniments emet? | filter:change, modal:open, form:valid |
| Quin estat manté? | Obert/tancat, valor seleccionat, pàgina actual |
| Com es destrueix? | Neteja de listeners, timers i observers |
Estructura de carpetes que escala sense framework
Una estructura simple pot ser suficient:
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
L’objectiu no és imitar un framework, sinó evitar acoblaments invisibles. Cada component hauria d’exposar una funció d’inicialització i, si escau, una funció de destrucció:
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));
};
}
Aquest patró obliga a pensar en ownership. El component no busca per tota la pàgina, no muta nodes aliens i no deixa listeners penjats.
Contractes abans que convencions fràgils
Quan un component falla en producció, gairebé sempre el problema és en un contracte implícit. Un dissenyador canvia una classe, un editor mou un bloc, un endpoint retorna un camp diferent i el component deixa de funcionar.
Per evitar-ho, cada component hauria de documentar:
- selector arrel;
- atributs
data-*que consumeix; - esdeveniments que escolta i emet;
- requisits d’accessibilitat;
- dependències de CSS o tokens;
- errors esperats;
- exemple mínim d’HTML.
Exemple de contracte:
<section data-component="lead-filter">
<button data-filter="enterprise">Empreses</button>
<button data-filter="agency">Agències</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));
Estat: local per defecte, compartit només quan aporta
Vanilla es complica quan l’estat compartit s’improvisa. Abans de crear un store global, convé preguntar si l’estat realment el necessita.
| Estat | On viuria | Motiu |
|---|---|---|
| Modal obert/tancat | Component | No afecta la resta |
| Filtre d’una llista | Component o URL | Pot necessitar deep-linking |
| Usuari autenticat | Capa d’aplicació | L’usen diverses peces |
| Carret o checkout | Store clar o framework | Estat crític i compartit |
Si un projecte comença a necessitar sincronització constant entre components, rutes dinàmiques i actualitzacions reactives, potser la pregunta ja no és “com fer-ho en vanilla”, sinó “quin framework redueix risc”.
Rendiment i càrrega progressiva
Un avantatge real de vanilla és poder carregar poc JavaScript. Però aquest avantatge es perd si cada pàgina importa un bundle comú amb interaccions que no usa. A Astro, per exemple, la filosofia d’illes ajuda precisament a enviar menys JavaScript al client quan la pàgina pot ser estàtica.
Un criteri pràctic:
- HTML funcional primer.
- CSS sense dependència de JavaScript sempre que sigui possible.
- JavaScript només per millorar interacció.
- Importació per pàgina o per component.
- Mesura de pes, INP i errors de consola.
Quan migrar a un framework
Migrar té sentit quan el cost de mantenir l’arquitectura manual supera el cost del framework. Senyals típiques:
- moltes peces depenen del mateix estat;
- hi ha rutes, permisos i vistes dinàmiques complexes;
- l’equip necessita un patró comú de testing;
- hi ha duplicació de lògica entre components;
- les interaccions depenen de dades en temps real;
- els canvis de producte trenquen peces no relacionades.
La migració no ha de ser total. Es pot aïllar primer la zona amb més complexitat i mantenir vanilla en pàgines estàtiques o components simples.
Lectures relacionades
- Performance i conversió: com llegir Core Web Vitals
- Refactorització web urgent: senyals, riscos i pla d’acció
- Plugins de WordPress el 2026: quan instal·lar i quan desenvolupar
Conclusió
Vanilla JS no és una excusa per renunciar a l’arquitectura. Al contrari: obliga a ser més conscient de contractes, límits i cost de manteniment. Si el projecte és simple, aquesta claredat pot ser un avantatge enorme. Si deixa de ser-ho, la mateixa arquitectura et donarà una ruta de migració menys traumàtica.
Preguntes freqüents
- Té sentit usar components sense React o Vue?
- Sí, si el producte té interaccions acotades, poca necessitat d'estat global i prioritat en rendiment. La clau és definir contractes, esdeveniments i límits de responsabilitat.
- Quan convé migrar des de vanilla JS a un framework?
- Convé migrar quan apareixen estat compartit complex, rutes riques, equips grans, reutilització intensa o proves que es tornen difícils amb mòduls solts.
- Què ha de tenir un component vanilla mantenible?
- Ha de tenir una API clara, estat local controlat, esdeveniments documentats, accessibilitat, proves mínimes i estils que no depenguin de selectors fràgils.