Throttling and debouncing are two techniques for controlling how often a function fires in response to high-frequency events like scroll, resize, mousemove, and input. Without them, a scroll listener on a page with a parallax effect or a sticky header can fire 60–100 times per second, blocking the main thread and causing janky animations. Debounce delays execution until the event has stopped firing for a specified wait period — ideal for search-as-you-type (fire the API call only after the user stops typing for 300ms). Throttle limits execution to at most once per specified interval regardless of how many events fire — ideal for scroll and resize handlers where you want regular updates but not every single pixel of movement. A debounced resize handler will only fire once the user has finished resizing; a throttled one fires every 200ms while they are resizing. Both are implemented with setTimeout/clearTimeout wrappers and can be written in about 10 lines each — no library required. For WordPress projects, this knowledge pairs directly with the debounce guide (which covers input specifically), the screen size detection guide, and the Intersection Observer guide — which replaces scroll listeners for most viewport-detection use cases entirely.
Problem: Scroll and resize event listeners in your WordPress theme fire hundreds of times per second, causing performance issues and janky animations.
Solution: Add the following utility functions to your theme’s JavaScript file:
// Debounce: delay until events stop
function debounce(fn, wait = 300) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
// Throttle: at most once per interval
function throttle(fn, limit = 200) {
let lastRun = 0;
return function(...args) {
const now = Date.now();
if (now - lastRun >= limit) {
lastRun = now;
fn.apply(this, args);
}
};
}
// Usage examples
// Throttled scroll: update sticky header class at most every 100ms
const handleScroll = throttle(() => {
const header = document.querySelector('.site-header');
if (!header) return;
header.classList.toggle('scrolled', window.scrollY > 80);
}, 100);
window.addEventListener('scroll', handleScroll, { passive: true });
// Debounced resize: recalculate layout only after resize finishes
const handleResize = debounce(() => {
console.log('Resize complete:', window.innerWidth, window.innerHeight);
// recalculate grid column widths, re-init sliders, etc.
}, 250);
window.addEventListener('resize', handleResize);
// Debounced search: fire AJAX only after user stops typing
const searchInput = document.querySelector('#live-search');
if (searchInput) {
searchInput.addEventListener('input', debounce(async (e) => {
const query = e.target.value.trim();
if (query.length < 3) return;
const res = await fetch(`/wp-json/wp/v2/posts?search=${encodeURIComponent(query)}&per_page=5`);
const posts = await res.json();
renderSearchResults(posts);
}, 350));
}
// requestAnimationFrame throttle (best for visual updates)
function rafThrottle(fn) {
let rafId = null;
return function(...args) {
if (rafId) return;
rafId = requestAnimationFrame(() => {
fn.apply(this, args);
rafId = null;
});
};
}
// Scroll handler synced to display refresh rate (60fps max)
window.addEventListener('scroll', rafThrottle(() => {
// Parallax, progress bar, or other visual-sync work here
}), { passive: true });
NOTE: Always add { passive: true } to scroll event listeners — this tells the browser the listener will never call preventDefault(), allowing it to start scrolling immediately on the GPU compositor thread without waiting for your JavaScript to run. Without passive: true, Chrome and Firefox show a console warning and scroll performance degrades. For animations tied to scroll position, requestAnimationFrame throttling (the rafThrottle pattern above) is better than a fixed millisecond interval because it syncs exactly to the display refresh rate — no wasted frames, no missed updates.