How to implement throttle with leading edge in JavaScript
Throttle with leading edge executes a function immediately on the first call, then enforces a cooldown period before allowing subsequent executions. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented throttle with leading edge for scroll handlers and button clicks that reduced event processing by 90% while maintaining immediate user feedback.
The most effective approach executes immediately on the first call with configurable trailing edge behavior.
Basic Throttle with Leading Edge
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => {
inThrottle = false
}, limit)
}
}
}
// Usage
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY)
}, 1000)
window.addEventListener('scroll', handleScroll)
Throttle with Leading and Trailing Options
function createThrottle(options = {}) {
return function throttle(func, limit) {
const { leading = true, trailing = false } = options
let lastRun = 0
let timeout
return function(...args) {
const now = Date.now()
// Leading edge execution
if (leading && now - lastRun >= limit) {
func.apply(this, args)
lastRun = now
}
// Trailing edge execution
if (trailing) {
clearTimeout(timeout)
timeout = setTimeout(() => {
if (now - lastRun >= limit) {
func.apply(this, args)
lastRun = Date.now()
}
}, limit - (now - lastRun))
}
}
}
}
// Leading edge only (immediate execution)
const leadingThrottle = createThrottle({ leading: true, trailing: false })
const handleClick = leadingThrottle(() => {
console.log('Button clicked')
}, 1000)
// Leading and trailing (execute immediately and at end)
const bothThrottle = createThrottle({ leading: true, trailing: true })
const handleInput = bothThrottle((e) => {
console.log('Input value:', e.target.value)
}, 500)
Advanced Throttle Implementation
function throttle(func, limit, options = {}) {
let lastRun = 0
let timeout
let lastArgs
const {
leading = true,
trailing = true
} = options
const invoke = function(context, args) {
lastRun = Date.now()
lastArgs = null
func.apply(context, args)
}
const throttled = function(...args) {
const now = Date.now()
const remaining = limit - (now - lastRun)
lastArgs = args
if (remaining <= 0 || remaining > limit) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
if (leading) {
invoke(this, args)
}
} else if (trailing && !timeout) {
timeout = setTimeout(() => {
invoke(this, lastArgs)
timeout = null
}, remaining)
}
}
throttled.cancel = () => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
lastRun = 0
lastArgs = null
}
throttled.flush = () => {
if (timeout && lastArgs) {
invoke(this, lastArgs)
clearTimeout(timeout)
timeout = null
}
}
return throttled
}
Usage Examples
Scroll Handler
const handleScroll = throttle(() => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
if (scrollTop > 300) {
document.getElementById('back-to-top').style.display = 'block'
} else {
document.getElementById('back-to-top').style.display = 'none'
}
}, 200, { leading: true, trailing: false })
window.addEventListener('scroll', handleScroll)
Resize Handler
const handleResize = throttle(() => {
console.log('Window size:', window.innerWidth, window.innerHeight)
// Recalculate layouts, update charts, etc.
}, 250, { leading: true, trailing: true })
window.addEventListener('resize', handleResize)
Button Click Protection
const submitForm = throttle(async (e) => {
e.preventDefault()
console.log('Submitting form...')
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: new FormData(e.target)
})
const result = await response.json()
console.log('Form submitted:', result)
} catch (error) {
console.error('Submission failed:', error)
}
}, 2000, { leading: true, trailing: false })
document.getElementById('form').addEventListener('submit', submitForm)
API Request Throttling
const searchAPI = throttle(async (query) => {
try {
const response = await fetch(`/api/search?q=${query}`)
const results = await response.json()
displayResults(results)
} catch (error) {
console.error('Search failed:', error)
}
}, 300, { leading: true, trailing: true })
input.addEventListener('input', (e) => {
searchAPI(e.target.value)
})
React Hook Implementation
import { useRef, useCallback } from 'react'
function useThrottle(callback, limit, options = {}) {
const { leading = true, trailing = true } = options
const lastRun = useRef(0)
const timeout = useRef(null)
const throttledCallback = useCallback(
(...args) => {
const now = Date.now()
const remaining = limit - (now - lastRun.current)
if (remaining <= 0 || remaining > limit) {
if (timeout.current) {
clearTimeout(timeout.current)
timeout.current = null
}
if (leading) {
lastRun.current = now
callback(...args)
}
} else if (trailing && !timeout.current) {
timeout.current = setTimeout(() => {
lastRun.current = Date.now()
callback(...args)
timeout.current = null
}, remaining)
}
},
[callback, limit, leading, trailing]
)
return throttledCallback
}
// Usage in component
function SearchComponent() {
const handleSearch = useThrottle(
(query) => {
console.log('Searching for:', query)
// Perform search
},
500,
{ leading: true, trailing: true }
)
return (
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
)
}
Performance Monitoring
function throttleWithStats(func, limit, options = {}) {
let execCount = 0
let skipCount = 0
const throttled = throttle(
(...args) => {
execCount++
func(...args)
},
limit,
options
)
return Object.assign(
(...args) => {
skipCount++
return throttled(...args)
},
{
getStats: () => ({
executions: execCount,
skipped: skipCount - execCount,
reduction: ((1 - execCount / skipCount) * 100).toFixed(2) + '%'
}),
resetStats: () => {
execCount = 0
skipCount = 0
}
}
)
}
// Usage
const handler = throttleWithStats(() => {
console.log('Executed')
}, 1000, { leading: true })
// Simulate many calls
for (let i = 0; i < 100; i++) {
handler()
}
console.log(handler.getStats())
// { executions: 1, skipped: 99, reduction: '99.00%' }
Throttle vs Debounce with Leading
// Throttle with leading - executes immediately, then enforces cooldown
const throttled = throttle(() => {
console.log('Throttled')
}, 1000, { leading: true, trailing: false })
// Debounce with leading - executes immediately, then waits for silence
const debounced = debounce(() => {
console.log('Debounced')
}, 1000, { leading: true, trailing: false })
// Rapid calls
for (let i = 0; i < 5; i++) {
setTimeout(() => {
throttled() // Executes at: 0ms
debounced() // Executes at: 0ms
}, i * 200) // Calls at: 0, 200, 400, 600, 800ms
}
// Throttle: 1 execution (at 0ms)
// Debounce: 1 execution (at 0ms)
Mouse Move Throttle
const handleMouseMove = throttle((e) => {
console.log('Mouse position:', e.clientX, e.clientY)
const cursor = document.getElementById('custom-cursor')
cursor.style.left = e.clientX + 'px'
cursor.style.top = e.clientY + 'px'
}, 16, { leading: true, trailing: false }) // ~60fps
document.addEventListener('mousemove', handleMouseMove)
Infinite Scroll
const loadMore = throttle(() => {
const scrollTop = window.pageYOffset
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
if (scrollTop + windowHeight >= documentHeight - 100) {
console.log('Loading more items...')
fetchMoreItems().then(items => {
appendItems(items)
})
}
}, 200, { leading: true, trailing: false })
window.addEventListener('scroll', loadMore)
Best Practice Note
This is the same throttle implementation we use in CoreUI’s scroll handlers and event-heavy components. Leading edge execution provides immediate user feedback while throttling prevents performance degradation from excessive function calls. Use throttle for continuous events (scroll, resize, mousemove) and debounce for discrete events (search input, form validation).
Related Articles
For related performance patterns, check out how to implement debounce with abort in JavaScript and how to create a memoization function in JavaScript.



