How to throttle API calls in Vue
Throttling API calls in Vue limits execution frequency for high-frequency events like scroll, resize, and mouse movement. With over 12 years of Vue.js experience since 2014 and as the creator of CoreUI, I’ve optimized performance with throttling in data-heavy dashboards. Throttling ensures a function executes at most once per specified time interval, unlike debouncing which delays execution. This approach prevents excessive API calls during continuous events while maintaining responsiveness.
Use throttle to limit API call frequency during high-frequency events like scroll, ensuring consistent execution intervals.
Basic throttle implementation:
<template>
<div @scroll='handleScroll' class='scrollable-container'>
<div class='content'>
<p v-for='item in items' :key='item'>{{ item }}</p>
</div>
<div v-if='loading'>Loading more...</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref(Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`))
const loading = ref(false)
let lastCallTime = 0
const throttleDelay = 1000 // Execute at most once per second
const loadMoreItems = async () => {
loading.value = true
await new Promise(resolve => setTimeout(resolve, 500))
const newItems = Array.from({ length: 20 }, (_, i) =>
`Item ${items.value.length + i + 1}`
)
items.value.push(...newItems)
loading.value = false
}
const handleScroll = (event) => {
const now = Date.now()
if (now - lastCallTime < throttleDelay) {
return // Skip execution
}
lastCallTime = now
const element = event.target
const scrolled = element.scrollTop + element.clientHeight
const total = element.scrollHeight
if (scrolled >= total - 100) {
loadMoreItems()
}
}
</script>
<style scoped>
.scrollable-container {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 20px;
}
</style>
Reusable throttle composable:
// useThrottle.js
import { ref } from 'vue'
export function useThrottleFn(fn, delay = 1000) {
let lastCallTime = 0
let timeoutId = null
const throttledFn = (...args) => {
const now = Date.now()
const timeSinceLastCall = now - lastCallTime
clearTimeout(timeoutId)
if (timeSinceLastCall >= delay) {
fn(...args)
lastCallTime = now
} else {
// Schedule execution at the end of the interval
timeoutId = setTimeout(() => {
fn(...args)
lastCallTime = Date.now()
}, delay - timeSinceLastCall)
}
}
return throttledFn
}
// Usage
<script setup>
import { useThrottleFn } from './useThrottle'
const fetchData = async () => {
const response = await fetch('/api/data')
const data = await response.json()
console.log('Data fetched:', data)
}
const throttledFetch = useThrottleFn(fetchData, 2000)
const handleScroll = () => {
throttledFetch()
}
</script>
Infinite scroll with throttle:
<template>
<div>
<div ref='scrollContainer' @scroll='handleScroll' class='scroll-container'>
<div v-for='post in posts' :key='post.id' class='post-card'>
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
</div>
<div v-if='loading' class='loading'>Loading...</div>
<div v-if='!hasMore' class='end-message'>No more posts</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const posts = ref([])
const loading = ref(false)
const hasMore = ref(true)
const page = ref(1)
let lastScrollTime = 0
const throttleDelay = 500
const fetchPosts = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
const response = await fetch(`/api/posts?page=${page.value}&limit=20`)
const data = await response.json()
if (data.length === 0) {
hasMore.value = false
} else {
posts.value.push(...data)
page.value++
}
} catch (error) {
console.error('Error fetching posts:', error)
} finally {
loading.value = false
}
}
const handleScroll = (event) => {
const now = Date.now()
if (now - lastScrollTime < throttleDelay) {
return
}
lastScrollTime = now
const container = event.target
const scrollPosition = container.scrollTop + container.clientHeight
const threshold = container.scrollHeight - 200
if (scrollPosition >= threshold) {
fetchPosts()
}
}
onMounted(() => {
fetchPosts()
})
</script>
<style scoped>
.scroll-container {
height: 600px;
overflow-y: auto;
padding: 20px;
}
.post-card {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.loading,
.end-message {
text-align: center;
padding: 20px;
color: #666;
}
</style>
Throttle window resize:
<template>
<div>
<div class='viewport-info'>
<p>Width: {{ windowWidth }}px</p>
<p>Height: {{ windowHeight }}px</p>
<p>Breakpoint: {{ breakpoint }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
let resizeTimeout = null
const throttleDelay = 250
const breakpoint = computed(() => {
if (windowWidth.value < 768) return 'mobile'
if (windowWidth.value < 1024) return 'tablet'
return 'desktop'
})
let lastResizeTime = 0
const updateDimensions = () => {
windowWidth.value = window.innerWidth
windowHeight.value = window.innerHeight
// Optional: Make API call on resize
// fetchResponsiveData()
}
const handleResize = () => {
const now = Date.now()
if (now - lastResizeTime < throttleDelay) {
return
}
lastResizeTime = now
updateDimensions()
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
Throttle with VueUse:
npm install @vueuse/core
<script setup>
import { ref } from 'vue'
import { useThrottleFn, useScroll } from '@vueuse/core'
const scrollContainer = ref(null)
const { y: scrollY } = useScroll(scrollContainer)
const fetchData = async () => {
const response = await fetch('/api/data')
const data = await response.json()
console.log('Fetched data')
}
const throttledFetch = useThrottleFn(() => {
if (scrollY.value > 500) {
fetchData()
}
}, 1000)
watch(scrollY, () => {
throttledFetch()
})
</script>
Throttle mouse movement:
<template>
<div
@mousemove='handleMouseMove'
class='tracking-area'
>
<div class='cursor-position'>
X: {{ mouseX }}, Y: {{ mouseY }}
</div>
<div class='api-calls'>API Calls: {{ apiCallCount }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mouseX = ref(0)
const mouseY = ref(0)
const apiCallCount = ref(0)
let lastCallTime = 0
const throttleDelay = 100 // 100ms = 10 calls per second max
const sendMousePosition = async (x, y) => {
apiCallCount.value++
await fetch('/api/track-mouse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x, y })
})
}
const handleMouseMove = (event) => {
mouseX.value = event.clientX
mouseY.value = event.clientY
const now = Date.now()
if (now - lastCallTime < throttleDelay) {
return
}
lastCallTime = now
sendMousePosition(mouseX.value, mouseY.value)
}
</script>
<style scoped>
.tracking-area {
height: 400px;
background: #f5f5f5;
padding: 20px;
cursor: crosshair;
}
.cursor-position,
.api-calls {
font-family: monospace;
font-size: 14px;
margin: 5px 0;
}
</style>
Throttle with lodash:
npm install lodash-es
<script setup>
import { throttle } from 'lodash-es'
const fetchData = async () => {
const response = await fetch('/api/data')
const data = await response.json()
}
// Leading: execute immediately, then throttle
const throttledFetch = throttle(fetchData, 1000, {
leading: true,
trailing: false
})
// Trailing: wait for interval before executing
const throttledFetch2 = throttle(fetchData, 1000, {
leading: false,
trailing: true
})
</script>
Debounce vs Throttle comparison:
<template>
<div>
<h3>Debounce (waits until user stops)</h3>
<input @input='debouncedSearch' placeholder='Debounce example' />
<p>Debounce calls: {{ debounceCount }}</p>
<h3>Throttle (executes at regular intervals)</h3>
<input @input='throttledSearch' placeholder='Throttle example' />
<p>Throttle calls: {{ throttleCount }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const debounceCount = ref(0)
const throttleCount = ref(0)
let debounceTimer = null
let lastThrottleTime = 0
const debouncedSearch = () => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debounceCount.value++
}, 500)
}
const throttledSearch = () => {
const now = Date.now()
if (now - lastThrottleTime >= 500) {
throttleCount.value++
lastThrottleTime = now
}
}
</script>
Best Practice Note
Use throttling for continuous events (scroll, resize, mouse move) where you need consistent execution intervals. Use debouncing for discrete events (search input, form validation) where you want to wait until activity stops. Throttle delay of 100-250ms works well for scroll events. Use requestAnimationFrame for visual updates instead of throttling. Cancel throttled functions in component cleanup to prevent memory leaks. VueUse provides production-ready throttle implementations. This is how we implement throttling in CoreUI for Vue—optimizing high-frequency event handlers, reducing unnecessary API calls, and maintaining responsive interfaces in data-intensive dashboards.



