9.1 Event Delegation
Dealing with events in JavaScript can require complex management to optimize performance and improve app architecture.
Optimizing event handling in JavaScript is critical for creating high-performance web applications, especially when dealing with a large number of elements. In this context, event delegation and avoiding unnecessary handlers are key players.
Event Delegation is a pattern where an event handler is set on a parent element instead of each individual child element. This allows efficient event management for a large number of child elements, since the handler is called only once for the parent, not for each item.
Example without event delegation:
<!DOCTYPE html>
<html>
<head>
<title>Event Delegation Example</title>
</head>
<body>
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<script>
const list = document.querySelector('ul');
const items = document.querySelectorAll('#list li');
items.forEach(item => {
item.addEventListener('click', function(event) {
const li = document.createElement('li');
li.innerText = "New item, event won't fire on it";
list.appendChild(li);
alert(`You clicked on: ${event.target.textContent}`);
});
});
</script>
</body>
</html>
Problem: if the list of items grows dynamically, you'll have to add handlers for each new item.
Solution — Example with event delegation:
<!DOCTYPE html>
<html>
<head>
<title>Event Delegation Example</title>
</head>
<body>
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<script>
const list = document.getElementById('list');
list.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
const li = document.createElement('li');
li.innerText = "New item, event will fire on it";
list.appendChild(li);
alert(`You clicked on: ${event.target.textContent}`);
}
});
</script>
</body>
</html>
Benefits:
- Handler is added only to the parent element
- Dynamically added elements automatically support the event handler
9.2 Preventing Unnecessary Handlers
To avoid performance degradation, it's important to minimize the number of event handlers, especially if they're attached to many elements or fired frequently. Here are some optimization strategies:
1. Reducing Handlers Count: use event delegation to cut down on the number of handlers.
2. Using once in addEventListener: if an event handler should run only once, use the option { once: true }
when adding the handler.
Example:
<!DOCTYPE html>
<html>
<head>
<title>One-time Event Example</title>
</head>
<body>
<button id="myButton">Click me!</button>
<script>
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
alert('You clicked the button');
}, { once: true });
</script>
</body>
</html>
3. Debouncing and Throttling: these techniques are useful for optimizing event handlers that fire frequently,
like scroll
or resize
.
9.3 Debouncing
Debouncing combines multiple sequential function calls into one, executed only after the event stream has ended.
Example:
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
window.addEventListener('resize', debounce(() => {
console.log('Window resized');
}, 300));
Try changing the widget width to see the result
Example of debouncing
In this example, the search function will only run once the user has stopped typing for 300 milliseconds.
<!DOCTYPE html>
<html>
<head>
<title>Debouncing Example</title>
</head>
<body>
<div style="min-height: 55px">
<input type="text" id="searchInput" placeholder="Start typing a query...">
<div id="results"></div>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const results = document.getElementById('results');
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
function performSearch(event) {
const query = event.target.value;
results.textContent = 'Searching for: ' + query;
// Simulating server request
setTimeout(() => {
results.textContent = 'You searched for: ' + query;
}, 500);
}
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener('input', debouncedSearch);
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Debouncing Example</title>
</head>
<body>
<input type="text" id="searchInput" placeholder="Start typing a query...">
<div id="results"></div>
<script>
const searchInput = document.getElementById('searchInput');
const results = document.getElementById('results');
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
function performSearch(event) {
const query = event.target.value;
results.textContent = 'Searching for: ' + query;
// Simulating server request
setTimeout(() => {
results.textContent = 'You searched for: ' + query;
}, 500);
}
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener('input', debouncedSearch);
</script>
</body>
</html>
9.4 Throttling
Trolling Throttling ensures that a function is called no more than once every specified interval of time.
Example:
function throttle(func, limit) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('Window scrolled');
}, 300));
Another Example of Throttling
In this example, the scroll handler is called no more than every 200 milliseconds, which helps prevent overloading the browser during frequent scroll events.
<!DOCTYPE html>
<html>
<head>
<title>Throttling Example</title>
</head>
<body>
<div id="scrollContainer" style="height: 200px; overflow-y: scroll;">
<div style="height: 1000px;"></div>
</div>
<div id="status">Start scrolling to see the effect</div>
<script>
const scrollContainer = document.getElementById('scrollContainer');
const status = document.getElementById('status');
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
function handleScroll(event) {
status.textContent = 'Scrolling... ' + new Date().toLocaleTimeString();
}
const throttledScroll = throttle(handleScroll, 200);
scrollContainer.addEventListener('scroll', throttledScroll);
</script>
</body>
</html>
9.5 Event Optimization with Passive Listeners
Passive event listeners are used to improve performance, especially when handling scrolling events (scroll
). When an event handler is set as passive, it means it won't call preventDefault()
, which allows the browser to optimize performance.
Example:
window.addEventListener('scroll', function(event) {
console.log('Scrolling');
}, { passive: true });
GO TO FULL VERSION