Debounce and throttle are two fundamental JavaScript performance patterns that control how often a function can be called in response to rapid events like keystrokes, scroll movements, and window resizes. Without them, a live search field that fires an API request on every keypress will send dozens of requests per second while the user is typing, and a scroll handler that recalculates layout on every scroll event will fire hundreds of times per second, locking the main thread and causing jank. Debounce delays execution until a specified quiet period has elapsed after the last event — if events keep firing, the timer resets. This is the right pattern for inputs where you want to wait until the user has stopped typing before acting. Throttle limits execution to at most once per specified interval regardless of how many events fire — it guarantees regular execution during continuous events. This is the right pattern for scroll and resize handlers where you want updates at a steady rate, not after a pause. Both patterns can be implemented in fewer than fifteen lines of vanilla JavaScript using setTimeout and clearTimeout for debounce, and a timestamp comparison for throttle. They are also available in utility libraries like Lodash (_.debounce, _.throttle) but writing them from scratch is straightforward and avoids a dependency. In WordPress themes the most common use cases are: debouncing the live search input that queries the REST API, throttling the scroll handler that updates a sticky header’s appearance, throttling the resize handler that recalculates a masonry grid layout, and debouncing a window resize handler that re-initialises a slider. Pair this with the Fetch API guide for live REST API search and the Intersection Observer guide for scroll-triggered effects that do not need throttling.
Problem: A live search input fires REST API requests on every keystroke and a scroll handler fires hundreds of times per second, causing excessive network requests and UI jank.
Solution: Wrap the search callback in debounce and the scroll callback in throttle:
/**
* debounce — delay execution until `wait` ms after the last call.
* Use for: search inputs, form validation, resize recalculations.
*/
function debounce( fn, wait ) {
let timer;
return function ( ...args ) {
clearTimeout( timer );
timer = setTimeout( () => fn.apply( this, args ), wait );
};
}
/**
* throttle — execute at most once every `limit` ms.
* Use for: scroll handlers, mousemove, resize visual updates.
*/
function throttle( fn, limit ) {
let lastRun = 0;
return function ( ...args ) {
const now = Date.now();
if ( now - lastRun >= limit ) {
lastRun = now;
fn.apply( this, args );
}
};
}
// Example 1: Live search — debounce 400ms
const searchInput = document.querySelector( '#live-search' );
const resultsList = document.querySelector( '#search-results' );
const handleSearch = debounce( async function ( e ) {
const query = e.target.value.trim();
if ( query.length < 2 ) { resultsList.innerHTML = ''; return; }
const res = await fetch( haRest.root + 'wp/v2/posts?search=' + encodeURIComponent( query ) + '&per_page=5&_fields=id,title,link' );
const posts = await res.json();
resultsList.innerHTML = posts
.map( p => `<li><a href="${p.link}">${p.title.rendered}</a></li>` )
.join( '' );
}, 400 );
searchInput && searchInput.addEventListener( 'input', handleSearch );
// Example 2: Sticky header on scroll — throttle 100ms
const header = document.querySelector( '.site-header' );
const handleScroll = throttle( function () {
header.classList.toggle( 'is-sticky', window.scrollY > 80 );
}, 100 );
window.addEventListener( 'scroll', handleScroll, { passive: true } );
// Example 3: Recalculate grid on resize — debounce 250ms
const handleResize = debounce( function () {
document.querySelectorAll( '.masonry-grid' ).forEach( recalculateMasonry );
}, 250 );
window.addEventListener( 'resize', handleResize );
NOTE: Use { passive: true } on scroll event listeners to tell the browser the handler will never call preventDefault() — this allows the browser to scroll immediately without waiting for the listener to finish, which is critical for smooth scrolling performance. For the live search example, always add a minimum character threshold (here query.length < 2) in addition to debouncing, to avoid sending requests for single characters. If you need the debounced function to fire immediately on the first call and then wait, add a leading: true option flag — this is the “leading edge” variant used in button click handlers to prevent double-submits.