Articulo publicado el
Cómo reducir listeners y mejorar el rendimiento usando bubbling de eventos
La gestión de eventos en DOM es una tarea cotidiana en el desarrollo frontend. Sin embargo, cuando trabajas con listas dinámicas, tablas grandes o estructuras complejas, agregar un listener a cada elemento puede convertirse rápidamente en un problema de rendimiento y mantenibilidad.
Event delegation es una técnica poderosa que resuelve exactamente este problema, aprovechando el bubbling natural de eventos en el DOM. En este post, exploraremos cómo funcionan, cuándo usarlos y cómo implementarlos en JavaScript vanilla.
Event delegation es un patrón de programación que aprovecha el mecanismo de bubbling de eventos del DOM. En lugar de adjuntar un listener a cada elemento individual, adjuntas un único listener a un contenedor padre que captura todos los eventos que burbujean desde sus descendientes.
El navegador ejecuta los listeners en esta fase:
window hasta el elemento target.window.Por defecto, addEventListener escucha en la fase de bubbling. Esto significa que si un usuario hace clic en un elemento hijo, el evento "burbujea" hacia arriba, permitiendo que un listener en el padre lo capture.
Imagina una lista de tareas donde los usuarios pueden hacer clic para marcar elementos como completados.
const tasks = document.querySelectorAll(".task-item");
tasks.forEach((task) => {
task.addEventListener("click", () => {
task.classList.toggle("completed");
});
});
Este enfoque funciona para elementos estáticos, pero si agregas nuevas tareas dinámicamente, el nuevo elemento no tendrá el listener asignado. Además, si tienes 10,000 tareas, crearás 10,000 listeners.
const taskList = document.querySelector(".task-list");
taskList.addEventListener("click", (event) => {
// Verifica que el clic ocurrió en un elemento con la clase 'task-item'
if (event.target.closest(".task-item")) {
const taskItem = event.target.closest(".task-item");
taskItem.classList.toggle("completed");
}
});
Ahora, sin importar cuántas tareas tengas o cuántas agregues dinámicamente, un único listener maneja todos los eventos.
event.target y element.closest()Para identificar qué elemento disparó el evento dentro del delegado, tienes dos herramientas principales:
event.targetDevuelve el elemento exacto que disparó el evento.
document.addEventListener("click", (event) => {
console.log(event.target); // El elemento que fue clickeado
});
element.closest(selector)Busca el elemento más cercano que coincida con el selector, ascendiendo en el árbol DOM. Si no encuentra coincidencia, devuelve null.
document.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (button) {
console.log("Se hizo clic en un botón:", button.textContent);
}
});
Usar closest() es más robusto que solo event.target porque funciona incluso si el usuario hace clic en elementos hijo (como un <span> dentro de un <button>).
const tableBody = document.querySelector("tbody");
tableBody.addEventListener("click", (event) => {
if (event.target.closest(".delete-btn")) {
const row = event.target.closest("tr");
const rowId = row.dataset.id;
deleteRow(rowId);
row.remove();
}
if (event.target.closest(".edit-btn")) {
const row = event.target.closest("tr");
openEditDialog(row.dataset.id);
}
});
const form = document.querySelector("form");
form.addEventListener("input", (event) => {
if (event.target.type === "email") {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(event.target.value);
event.target.classList.toggle("invalid", !isValid);
}
if (event.target.type === "number") {
const isValid = event.target.value > 0;
event.target.classList.toggle("invalid", !isValid);
}
});
const nav = document.querySelector("nav");
nav.addEventListener("click", (event) => {
const menuItem = event.target.closest(".menu-item");
if (!menuItem) return;
const submenu = menuItem.querySelector(".submenu");
if (submenu) {
submenu.classList.toggle("visible");
event.preventDefault();
}
});
No todos los eventos burbujean. Algunos eventos importantes que no burbujean incluyen:
focus / blurload / unloadscrollresizereset / submit (en algunos navegadores)play / pause (audio/video)mouseenter / mouseleavePara eventos que no burbujean, necesitas usar la fase de capturing o adjuntar listeners directamente a los elementos.
// Para focus, debes usar el tercer parámetro (capturing phase)
document.addEventListener(
"focus",
(event) => {
console.log("Elemento enfocado:", event.target);
},
true
); // true = capturing phase
Veamos cómo event delegation mejora el rendimiento:
console.time("Sin delegation");
const items = document.querySelectorAll(".item");
items.forEach((item) => {
item.addEventListener("click", () => {
item.classList.add("active");
});
});
console.timeEnd("Sin delegation");
console.time("Con delegation");
const container = document.querySelector(".container");
container.addEventListener("click", (event) => {
if (event.target.closest(".item")) {
event.target.closest(".item").classList.add("active");
}
});
console.timeEnd("Con delegation");
Con 1,000 elementos:
En aplicaciones con listas dinámicas, la diferencia es aún más marcada porque con delegation, los nuevos elementos funcionan instantáneamente sin código adicional.
focus, blur o scroll, la delegation no es una opción.// ❌ No hagas esto: captura TODOS los clics del documento
document.addEventListener("click", (event) => {
// Lógica excesivamente genérica
});
Es mejor adjuntar el listener al contenedor más específico:
// ✅ Mejor: limita el scope al contenedor relevante
const userList = document.querySelector(".user-list");
userList.addEventListener("click", (event) => {
// Lógica enfocada
});
Event delegation es una técnica esencial para escribir JavaScript vanilla eficiente. Cuando la uses correctamente:
La clave está en elegir el contenedor correcto, usar event.target.closest() para identificar elementos, y recordar que no todos los eventos burbujean.
¿Utilizas event delegation en tus proyectos? Comparte tu experiencia en comentarios.
Estos articulos pueden interesarte si te gusto este articulo.
Optimiza el rendimiento de tu aplicación limitando cuántas veces se ejecutan listeners de eventos y funciones costosas
Maneja eventos en múltiples elementos con un único listener y mejora el rendimiento de tu aplicación
Aprende a usar los atributos data-* para guardar información en elementos HTML y accederla desde JavaScript de forma segura y semántica