Event Delegation & DOM Optimization
18 min read
Event DelegationEvent BubblingDocumentFragmentIntersectionObserverMutationObserverDOM PerformancestopPropagation
Event Propagation
When an event fires, it travels through the DOM in three phases:
- Capture Phase: Window → Document → ... → Target's parent
- Target Phase: The element itself
- Bubble Phase: Target's parent → ... → Document → Window
Event Delegation
Instead of adding event listeners to every child, add ONE listener to a parent and use event.target to identify which child was clicked.
// ❌ Bad: 1000 listeners for 1000 items
document.querySelectorAll("li").forEach(li => {
li.addEventListener("click", handleClick);
});
// ✅ Good: 1 listener on the parent
document.querySelector("ul").addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
handleClick(e.target);
}
});stopPropagation vs preventDefault
| Method | Does | Example |
|---|---|---|
stopPropagation() | Stops event from bubbling up | Prevent parent click handler from firing |
preventDefault() | Stops default browser action | Prevent form submission, link navigation |
stopImmediatePropagation() | Stops other listeners on same element | Multiple handlers on same element |
DOM Manipulation Performance
// ❌ Bad: Causes reflow on EVERY iteration
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
document.getElementById("list").appendChild(li); // 1000 reflows!
}
// ✅ Good: DocumentFragment — ONE reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById("list").appendChild(fragment); // 1 reflow!Batch Style Changes
// ❌ Bad: Triggers reflow for each style change
element.style.width = "100px";
element.style.height = "200px";
element.style.background = "red";
// ✅ Good: One class change = one reflow
element.classList.add("styled-box");Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll("img[data-src]").forEach(img => observer.observe(img));Mutation Observer
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log("DOM changed:", mutation.type);
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});