JavaScript 防抖(debounce)与节流(throttle)
Category(分类): JavaScript Status: 已整理(2026)
原文标题中的
thorttle是拼写错误,本文只修正显示标题,不改文件名,以免影响已有路由或文章索引。
**防抖(debounce)和节流(throttle)**都可以限制高频调用,但它们解决的问题不同:
- 防抖关注“一段时间没有新调用之后再执行”,常用于只关心最后状态的搜索、自动保存和窗口尺寸变化;
- 节流关注“在一个时间窗口内最多执行一次”,常用于以受控频率处理滚动、拖拽或指针移动;
- 对视觉更新,
requestAnimationFrame()往往比固定毫秒节流更合适;对元素尺寸变化,可以优先考虑ResizeObserver;对可见性,可以考虑IntersectionObserver。
它们只能控制发起调用的频率,不能自动保证异步请求按发起顺序返回,也不能替代缓存、取消请求、版本校验和正确的生命周期清理。
一、为什么需要限制高频调用
click、scroll、resize、input、mousemove 和 touchmove 等事件可能在短时间内触发很多次。如果每次回调都执行昂贵的计算、读取和修改布局,或者都发起网络请求,就可能增加主线程工作和网络压力。
原文用“修改 DOM 会引起回流(Reflow)和重排(Repaint)”概括这一点,但术语和因果关系不够准确:
- **布局/回流(layout/reflow)**是重新计算几何关系;
- **绘制(paint/repaint)**是把元素绘制到图层;
- **合成(composite)**是把图层合成为最终画面;
- 一次 DOM 修改不必然触发整页布局或重绘,真正的开销应通过浏览器 Performance 面板测量;
- “写入布局属性后立刻读取布局信息”可能触发强制同步布局,批量读写和合理的帧调度通常更重要。
如果回调会请求后端,防抖/节流也不是并发控制方案。旧请求可能在新请求之后返回,应该配合 AbortController、请求序列号或服务端版本号,只应用当前版本的结果。
二、防抖(Debounce)
防抖:连续调用发生时不断重新计时;只有在指定时间内没有新的调用,才执行一次。下面的通用实现默认支持尾触发,也可以配置为首触发。
原文中的第一版是“停止触发后执行”(尾触发防抖),第二版是“第一次立即执行,等待窗口结束”(首触发防抖)。这两种行为都合理,但必须明确是否执行最后一次调用。

图片来源:原文 Juejin 图床资源(2018 年前后),仅保留作历史配图,不代表当前库的实现或性能结论。
2.1 一个可取消、可 flush 的防抖实现
function debounce(func, wait = 0, options = {}) {
if (typeof func !== 'function') {
throw new TypeError('func 必须是函数')
}
const delay = Math.max(0, wait)
const { leading = false, trailing = true } = options
let timer = null
let lastArgs
let lastThis
let result
const invoke = () => {
const args = lastArgs
const thisArg = lastThis
lastArgs = undefined
lastThis = undefined
result = func.apply(thisArg, args)
return result
}
const later = () => {
timer = null
if (trailing && lastArgs) invoke()
lastArgs = undefined
lastThis = undefined
}
function debounced(...args) {
const callNow = leading && timer === null
lastArgs = args
lastThis = this
if (timer !== null) clearTimeout(timer)
timer = setTimeout(later, delay)
if (callNow) return invoke()
return result
}
debounced.cancel = () => {
if (timer !== null) clearTimeout(timer)
timer = null
lastArgs = undefined
lastThis = undefined
}
debounced.flush = () => {
if (timer === null) return result
clearTimeout(timer)
timer = null
if (trailing && lastArgs) invoke()
lastArgs = undefined
lastThis = undefined
return result
}
return debounced
}
这个实现的选项含义如下:
leading: false, trailing: true:默认尾触发,停止调用后执行最后一次;leading: true, trailing: false:首触发,窗口内调用被忽略;leading: true, trailing: true:第一次立即执行,窗口内如果有新调用,窗口结束时再执行最后一次;cancel():取消待执行的调用,组件卸载或页面销毁时应调用;flush():立即执行当前待处理的尾调用。
2.2 搜索建议:防抖不等于请求结果排序
搜索框应监听 input,这样可以覆盖键盘输入、粘贴和自动填充等情况。输入法组合输入还应根据产品需要处理 compositionstart/compositionend。
const input = document.querySelector('#search')
const resultList = document.querySelector('#results')
let revision = 0
let controller = null
const search = debounce(async query => {
const currentRevision = revision
controller?.abort()
controller = new AbortController()
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const data = await response.json()
// 即使旧请求没有及时响应,也不让它覆盖新输入的结果
if (currentRevision !== revision) return
resultList.replaceChildren(
...data.items.map(item => {
const element = document.createElement('li')
element.textContent = item.title
return element
})
)
} catch (error) {
if (error.name !== 'AbortError' && currentRevision === revision) {
console.error('搜索失败', error)
}
}
}, 250)
input.addEventListener('input', event => {
revision += 1
const query = event.target.value.trim()
if (!query) {
controller?.abort()
resultList.replaceChildren()
search.cancel()
return
}
search(query)
})
// 在组件卸载或页面销毁时执行
window.addEventListener('pagehide', () => {
search.cancel()
controller?.abort()
})
这里的 250ms 只是示例,实际值应通过接口延迟、输入体验和服务端负载测试确定。自动保存还应在 pagehide 或应用销毁前使用 flush()/同步持久化策略,不能只依赖一个未来的定时器。
2.3 其他防抖场景
- 窗口尺寸变化:连续调整窗口时只在停止调整后重新布局;元素自身尺寸变化则优先考虑
ResizeObserver; - 自动保存:用户停止编辑后保存草稿,但需要处理页面关闭、网络失败和保存版本冲突;
- 校验和格式化:在用户停止输入后校验,避免每个字符都触发昂贵操作;
- 搜索建议:减少请求次数,同时必须处理请求取消和过期结果;
- 不要把“几秒钟”当作固定建议:搜索建议通常需要经过 UX 验证的短窗口,而不是盲目等待数秒。
三、节流(Throttle)
节流:无论在时间窗口内调用多少次,函数最多按指定频率执行。实现可以选择首触发、尾触发或两者同时启用。

图片来源:原文 Juejin 图床资源(2018 年前后),仅保留作历史配图。
3.1 使用单调时钟的节流实现
Date.now() 可能受到系统时钟校正影响。计算持续时间时可以使用 performance.now();它是相对页面时间 origin 的单调时钟,更适合测量间隔。下面实现支持首触发、尾触发、cancel() 和 flush():
function throttle(func, wait = 0, options = {}) {
if (typeof func !== 'function') {
throw new TypeError('func 必须是函数')
}
const delay = Math.max(0, wait)
const { leading = true, trailing = true } = options
let timer = null
let lastInvokeTime = null
let lastArgs
let lastThis
let result
const invoke = now => {
lastInvokeTime = now
const args = lastArgs
const thisArg = lastThis
lastArgs = undefined
lastThis = undefined
result = func.apply(thisArg, args)
return result
}
const later = () => {
timer = null
if (trailing && lastArgs) invoke(performance.now())
lastArgs = undefined
lastThis = undefined
}
function throttled(...args) {
const now = performance.now()
if (lastInvokeTime === null && !leading) lastInvokeTime = now
lastArgs = args
lastThis = this
const remaining = lastInvokeTime === null ? 0 : delay - (now - lastInvokeTime)
if (remaining <= 0 || remaining > delay) {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
invoke(now)
} else if (timer === null && trailing) {
timer = setTimeout(later, remaining)
}
return result
}
throttled.cancel = () => {
if (timer !== null) clearTimeout(timer)
timer = null
lastInvokeTime = null
lastArgs = undefined
lastThis = undefined
}
throttled.flush = () => {
if (timer === null) return result
clearTimeout(timer)
timer = null
if (trailing && lastArgs) invoke(performance.now())
lastArgs = undefined
lastThis = undefined
return result
}
return throttled
}
原文的两个节流版本分别是“定时器首触发”和“时间戳首触发”,它们都会丢弃窗口内的中间调用,也没有尾触发和取消接口。实际使用时应明确这些语义,而不要只写“节流会处理所有中间状态”。
3.2 滚动与指针移动:先判断任务类型
如果任务只是按浏览器绘制节奏更新视觉状态,requestAnimationFrame() 通常比固定 16.6ms 节流更贴合渲染时机。屏幕可能是 60Hz、90Hz、120Hz 或更高,后台页面还可能被降频,因此 16.6ms 只是 60Hz 的理论帧间隔,不是浏览器保证的固定预算。
let frameId = null
let latestScrollY = window.scrollY
function updateIndicator(scrollY) {
document.querySelector('#progress').style.transform =
`scaleX(${Math.min(1, scrollY / document.body.scrollHeight)})`
}
function scheduleScrollUpdate() {
latestScrollY = window.scrollY
if (frameId !== null) return
frameId = requestAnimationFrame(() => {
frameId = null
updateIndicator(latestScrollY)
})
}
window.addEventListener('scroll', scheduleScrollUpdate, { passive: true })
// 页面或组件销毁时
function cleanup() {
window.removeEventListener('scroll', scheduleScrollUpdate)
if (frameId !== null) cancelAnimationFrame(frameId)
}
passive: true 表示监听器不会调用 preventDefault(),浏览器可以更早处理滚动;如果确实需要阻止默认滚动,不能把该监听器声明为 passive。对于不需要每帧处理的业务逻辑,仍可以使用普通节流。
元素尺寸变化示例:
const panel = document.querySelector('.panel')
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
console.log('新的内容盒尺寸:', entry.contentRect.width, entry.contentRect.height)
}
})
observer.observe(panel)
// 不再需要时:observer.disconnect()
3.3 其他节流场景
- 拖拽、
pointermove、mousemove和touchmove中的昂贵计算; - 游戏或模拟程序中按受控频率处理输入;
- 滚动事件中的非视觉业务逻辑;
- 上传进度、日志和遥测上报,但应同时考虑批量和丢弃策略。
滚动处理并不是“必须使用节流”:有些任务适合 requestAnimationFrame,有些任务适合 IntersectionObserver,有些任务只需要 CSS。应先测量回调本身的工作量和长任务,再选择方案。
四、如何选择
A debounce is utilized when you only care about the final state. A throttle is best used when you want to handle all intermediate states but at a controlled rate.
可以这样理解:
- 只关心一轮连续操作的最终状态:优先考虑防抖;
- 需要持续响应中间状态,但可以降低频率:考虑节流;
- 需要与屏幕绘制同步:优先考虑
requestAnimationFrame; - 需要观察尺寸或可见性变化:考虑
ResizeObserver或IntersectionObserver; - 需要避免旧网络结果覆盖新结果:增加取消、序列号或版本校验。
延迟和频率如何确定

图片来源:原文 Juejin 图床资源(2018 年前后),仅保留作历史配图。
不要把所有设备都按 60FPS 处理。回调必须在实际帧预算内完成,帧预算还包括样式计算、布局、绘制和合成;如果回调超过预算,就可能产生长任务和丢帧。使用 Performance 面板、Long Tasks、requestAnimationFrame 时间戳和真实设备测试,比背诵 16.6ms 更可靠。
五、参考资料与原文
- MDN:Window.requestAnimationFrame()
- MDN:Window.setTimeout()
- MDN:EventTarget.addEventListener()
- MDN:AbortController
- MDN:ResizeObserver
- MDN:IntersectionObserver
- Lodash:debounce
- Lodash:throttle
- WHATWG:Timers
- 原文:Debouncing and Throttling in JavaScript
- 原文作者文章:稀土掘金
本地图片来源:
images/27-image-01.webp:原文 Juejin 图床资源,资源标识b279fd94627d4ba48e676fbd0c6769d8;images/27-image-02.webp:原文 Juejin 图床资源,资源标识34e5e1e7d2c242fdb337eba492d6f39b;images/27-image-03.webp:原文 Juejin 图床资源,资源标识1a2280bf9dd843619958262120ea57af。