TanStack Pacer:终于自带可观察状态的防抖/节流库

TanStack Pacer 2026 完全指南:防抖、节流、限流、队列、批量处理模式的真实生产代码。5700万次下载,自带可观察状态。

Huifer
Huifer
September 18, 20269 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,TanStack Ship 的独立开发者与维护者。 自 2024 年以来,我在 Cloudflare Workers 上线了 12+ 个 SaaS 应用。2025 年 Q4,我在一次 Stripe API 调用中遇到了限流错误——计费控制台在页面加载时发出 40+ 次请求/秒。用 TanStack Pacer 的限流器重写后,45 分钟搞定,这个方案在 3 个应用上稳定运行了 8 个月。我还在所有搜索框用防抖、滚动追踪用节流、Webhook 批量处理用队列模式。这些模式目前在 TanStack Ship 上生产运行。

验证来源:TanStack Pacer on npm · TanStack Pacer Official Docs · TanStack Pacer GitHub · TanStack Store 最后更新:2026-09-18 · Changelog

TL;DR: TanStack Pacer(@tanstack/pacer,约 5700 万次下载,周下载约 340 万)提供防抖、节流、限流、队列、批量处理五种模式,一套统一 API,自带可观察状态。一次引入,一套心智模型,支持 React 和 Solid。这是完整生产级指南。


让我不得不引入库的问题

2025 年 11 月,我的计费控制台在页面加载的前 200ms 内产生了 40–60 次 API 调用。组件在各订阅同时解析时触发事件,每个都触发一次变更。Stripe API 在加载时返回 429 Too Many Requests,静默丢弃了计费事件。

我的第一个修复是 6 行基于 setTimeout 的防抖。撑了一周。然后我需要在 UI 里显示 pending 状态,后来又需要取消功能,再后来在滚动追踪里需要节流而不是防抖。6 行代码变成了 60 行脆弱的定时器管理,散布在 4 个文件里。

装了 TanStack Pacer。60 行变成 12 行,pending 状态是白送的,所有计时模式共用同一套 API。

这就是它的价值:不只是定时器,而是围绕它的一整套可观察模型。


TanStack Pacer 到底做了什么

TanStack Pacer 提供五种执行时机策略。选对策略只需要回答一个问题:你的工作流承受得起丢弃什么?

策略保留下代价适用场景
防抖最新的那一次调用中间的调用被丢弃搜索输入、自动保存
节流规律的采样中间的调用被丢弃滚动追踪、resize 处理器
限流时间窗口内的调用超出的调用被拒绝外部 API 保护
队列每一个任务顺序执行,无并发文件上传、顺序处理
批量每一个条目分组执行埋点写入、批量操作

核心洞察:所有五种模式都基于同一个可观察状态模型。Pacer 建立在 TanStack Store 之上,所以每个工具函数都将执行计数、pending 状态、错误状态暴露为响应式值,可以直接在 UI 中渲染——无需单独的状态管理。

安装方式:

bash
npm install @tanstack/pacer @tanstack/react-pacer

防抖:等待安静时刻

防抖延迟执行,直到调用停止到达配置的时间间隔。一波调用中只有最后一次运行。在中间调用无意义——只需要最终值——的场景使用它。

经典用例:搜索自动补全。 用户输入 "tanstack" 在 400ms 内击键 8 次。不防抖:8 次 API 调用。300ms 防抖:1 次 API 调用。

typescript
import { debounce } from '@tanstack/pacer'

const search = debounce(
  async (query: string) => {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
    if (!res.ok) throw new Error(`Search failed: ${res.status}`)
    return res.json()
  },
  { wait: 300 },
)

// 400ms 内的 8 次调用 → 静默 300ms 后只有最后一次触发
search('t')
search('ta')
search('tan')
search('tans')
search('tanst')
search('tansta')
search('tanstac')
search('tanstack')

React 版本与响应式状态集成:

typescript
import { useDebounce } from '@tanstack/react-pacer'
import { useQuery } from '@tanstack/react-query'

function SearchInput() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)

  const { data } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () => searchApi(debouncedQuery),
    enabled: debouncedQuery.length > 0,
  })

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Search..."
    />
  )
}

这个 hook 直接给你防抖后的响应式值。无需 useEffect,无需 setTimeout 清理,无陈旧闭包 bug。


节流:保持稳定节奏

节流每个时间间隔最多触发一次,均匀分布。在每次调用都有意义但需要控制频率时使用——节流对信号采样,防抖等待静默。

最适合:滚动事件处理器、resize 观察器、埋点追踪。 用户的滚动位置始终有意义,只是你不需要每秒 200 个事件。

typescript
import { throttle } from '@tanstack/pacer'

const trackScroll = throttle(
  (position: number) => {
    analytics.track('scroll_depth', { position })
  },
  { wait: 500, leading: true, trailing: true },
)

window.addEventListener('scroll', () => {
  trackScroll(window.scrollY)
})

leading: true 在首次调用时立即触发。trailing: true 在静默期结束后触发一次。两个都开,你会得到首次调用加上一波结束后的最后一次调用——适合滚动追踪,起点和终点都不想漏。


限流:保护外部 API

限流强制硬上限:时间窗口内 N 次调用。超出的调用被拒绝,不会延迟。当外部服务规定了配额时使用——Stripe、OpenAI、第三方 Webhook API。

这就是 2025 年 11 月修复我计费控制台的那一段:

typescript
import { createRateLimiter } from '@tanstack/pacer'

// Stripe 允许 Balance API 每秒 100 次读请求
const stripeLimiter = createRateLimiter({
  limit: 100,
  window: 1000, // 1 秒
})

async function fetchBalance() {
  const allowed = await stripeLimiter.maybeExecute(async () => {
    return stripe.balance.retrieve()
  })

  if (!allowed.ok) {
    // 触发限流 → 调度重试
    console.warn(`Rate limited, retry in ${allowed.retryAfter}ms`)
    setTimeout(fetchBalance, allowed.retryAfter)
    return null
  }

  return allowed.value
}

maybeExecute 返回一个结果,告诉你调用是运行了还是被拒绝了,同时给出 retryAfter 值用于调度退避。这是可观察的:你可以把拒绝计数绑定到 UI 指示器上,无需额外状态。


队列:每一次调用都必须运行

队列按顺序执行每个任务,可配置并发数。在不能丢弃调用时使用——文件上传、顺序 Webhook 处理、订单确认流程。

typescript
import { Queue } from '@tanstack/pacer'

const uploadQueue = new Queue({
  concurrency: 2,     // 2 个上传并行
  maxWait: 30000,    // 每个条目 30s 超时
  onError: (error, item) => {
    console.error(`Upload failed for ${item.name}:`, error)
    notifyUser(`Upload of ${item.name} failed`)
  },
  onSettled: (result, item) => {
    updateProgress(item.id, 'complete')
  },
})

// 从拖放处理器添加文件
fileList.forEach(file => {
  uploadQueue.add(async () => {
    updateProgress(file.id, 'uploading')
    const formData = new FormData()
    formData.append('file', file)
    const res = await fetch('/api/upload', { method: 'POST', body: formData })
    if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
    return res.json()
  })
})

uploadQueue.start()

// 在 UI 中可以读取队列状态:
uploadQueue.store.subscribe(state => {
  renderProgress({ pending: state.pending, settled: state.settled })
})

队列条目支持优先级排序,队列可以暂停、恢复、清空。如果用户中途取消批量上传,queue.clear() 丢弃待处理项,queue.pause() 停止新启动但不丢弃正在执行的工作。


可观察状态:别人都没有的功能

大多数防抖/节流实现只是一个定时器加一个函数。Pacer 建立在 TanStack Store 之上,将执行状态暴露为可观察值:

typescript
import { debounce } from '@tanstack/pacer'

const search = debounce(searchApi, { wait: 300 })

// 直接读取响应式状态
search.store.subscribe(state => {
  console.log('Pending:', state.isPending)
  console.log('Execution count:', state.executionCount)
  console.log('Error:', state.error)
})

// 在 React 中无需 useState 渲染
function SearchStatus() {
  const { isPending, executionCount } = useStore(search.store)
  if (!isPending && executionCount === 0) return null
  return (
    <span>
      {isPending ? 'Searching...' : `Searched ${executionCount} times`}
    </span>
  )
}

这就是实际差异:防抖结果不只是一个可调用函数,它是一个实时数据源。pending 微调框、执行计数器、错误显示——全部从同一个对象取数,无需额外的 useState。


异步变体:支持重试与中止

对于必须成功的操作——保存草稿、处理支付意图、同步记录——使用异步变体:

typescript
import { asyncDebounce } from '@tanstack/pacer'

const saveDraft = asyncDebounce(
  async (content: string) => {
    const res = await fetch('/api/drafts', {
      method: 'POST',
      body: JSON.stringify({ content }),
    })
    if (!res.ok) throw new Error(`Save failed: ${res.status}`)
    return res.json()
  },
  {
    wait: 1000,
    asyncRetryerOptions: {
      maxAttempts: 3,
      backoff: 'exponential',
      baseWait: 500,
      jitter: 0.1,
    },
  },
)

// 在 React 自动保存 hook 中:
const handleChange = async (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  setContent(e.target.value)
  await saveDraft(e.target.value) // 失败时自动重试
}

重试器使用带抖动的指数退避——适合瞬态网络错误。对于持续失败的情况,onError 回调处理用户通知。将 signal 传给底层 fetch 调用以支持取消时中止。


React Hooks:完整模式集

TanStack Pacer 提供一等 React hooks。以下是我每个搜索框都用的生产模式:

typescript
import { useDebouncedCallback, useDebounce } from '@tanstack/react-pacer'
import { useQuery } from '@tanstack/react-query'

function ProductSearch() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 350)

  const { data, isPending, error } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () => searchProducts(debouncedQuery),
    enabled: debouncedQuery.length >= 2,
    staleTime: 30_000,
  })

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      {isPending && <Spinner />}
      {error && <ErrorBanner error={error} />}
      {data && <ResultsList products={data} />}
    </div>
  )
}

useDebounce hook 给你一个防抖后的响应式值,可以直接传给 useQuery 的 queryKey。这意味着 TanStack Query 自动处理缓存和后台重新获取——Pacer 管时机,Query 管数据获取。不需要手动协调。


TanStack Pacer vs 竞品方案

JavaScript 生态里不缺 debounce 实现。以下是 Pacer 的对比:

功能Pacerlodash.debounce自定义 setTimeout
可观察状态✅ TanStack Store❌❌
DevTools✅❌❌
五种模式全支持✅仅防抖/节流❌
框架 hooks✅ React + Solid❌❌
树摇✅ 深度导入部分N/A
Beta 状态是(API 稳定)稳定N/A
核心包体积~3KB~1.5KB0KB

Pacer 不是最小的选择。脚本标签里一次性防抖,lodash 依然够用。对于生产应用,有多种计时模式、可观察状态、框架集成的需求,Pacer 是统一的选择——一次引入,一套心智模型,一个状态来源。


生产检查清单

上线任何 Pacer 实现前:

  • 选对策略。 搜索用防抖、滚动用节流、API 保护用限流、顺序工作用队列。
  • 保守设置等待间隔。 搜索标准 300ms,节流 500ms,根据你的 p95 加载时间调优。
  • 限流时处理被拒绝状态。 被限流的调用需要重试路径,不是静默失败。
  • 必须成功的操作用异步变体。 同步防抖无法 await 或重试。
  • 可取消操作传 abort signal。 长运行请求应在组件卸载时停止。
  • 升级前阅读 beta API 文档。 API 稳定但 1.0 之前仍有小幅变更可能。

相关阅读: Tanstack Hotkeys Complete Guide 2026 · Tanstack Form Complete Guide · Tanstack Virtual Complete Guide 2026

什么时候等 Pacer

TanStack Pacer 处于 beta 阶段。如果你在评估它用于新项目,beta 状态意味着:

  • API 不太可能破坏性变更,但升级时要审查 changelog。
  • React 和 Solid 适配器已生产就绪。Angular、Preact、Svelte 和 Vue 适配器需要贡献者。
  • pacer-lite 变体提供不带 reactivity hooks 的相同 API——适合发布库。

对于今天就要上线给真实用户的项目,beta 是安全的。对于需要硬性 1.0 承诺的团队,lodash 是保守选择。


TanStack Ship 所有新项目默认集成 Pacer。 如果你在评估 SaaS 样板,看 Pacer 如何与 TanStack Start 路由模型 集成,以及完整功能列表。