ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 28

  1. Blog

perfect-debounce vs lodash.debounce: 447 B vs 1.2 KB

Aditya Challa·5 October 2026·5 min read

Hands-on
On this page
  1. What I tested
  2. How much smaller is perfect-debounce?
  3. Do trailing, leading, cancel and flush match?
  4. Is invoke speed a reason to pick lodash?
  5. Which one should you pick?
  6. Sources
  7. Related

I bundled the two common debounce helpers on this box: perfect-debounce 2.1.0 came out at 447 bytes gzip, while lodash.debounce 4.0.8 came out at 1,163 bytes. Both fired once after a trailing burst. The bigger differences were API shape, not the microsecond invoke cost.

Short answer: pick perfect-debounce when you want a tiny, promise-returning debounce for async work. Keep lodash.debounce when you need maxWait or the classic leading-plus-trailing double fire. No affiliate links in this post.

What I tested

  • Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 01:40 IST.
  • Versions: perfect-debounce 2.1.0 (published Jan 2026), lodash.debounce 4.0.8 (last published Jun 2022), Node 22.20.0, esbuild 0.28.2 browser ESM minify + gzip -9.
  • Inputs: synthetic create and invoke loops (20,000 wrappers; 50,000 rapid calls at wait 0 and wait 16 ms), plus leading/trailing/cancel/flush/promise checks. Scripts and results are in /workspace/bench-perfect-debounce-lodash/.
  • Method: median of 9 rounds after warm-up, two runs with the library order reversed; separate async behavior harness.
  • Findings: gzip 447 B vs 1,163 B. Create cost was similar (~0.08–0.13 µs each). Rapid invoke favored lodash (~0.07–0.11 µs vs ~0.23–0.30 µs), which does not matter once wait is tens of milliseconds.
  • One surprise: with leading and trailing both true, lodash fired twice on a burst; perfect-debounce fired once (leading). Their option matrices are not drop-in identical.
  • Not tested: browsers, React 18 concurrent renderers, requestAnimationFrame wrappers, or lodash-es tree-shaken from the full lodash package.

How much smaller is perfect-debounce?

Build (esbuild browser minify)Minifiedgzip -9
perfect-debounce715 B447 B
lodash.debounce2,351 B1,163 B

About 2.6× smaller gzip. That is the number I would put on a PR when someone imports debounce into a client bundle. Track it with size-limit vs bundlesize. If debounce is the last lodash holdout, the same cleanup pattern shows up in es-toolkit vs lodash and defu vs lodash.merge.

Do trailing, leading, cancel and flush match?

Caseperfect-debounce 2.1.0lodash.debounce 4.0.8
5 rapid calls, trailing default1 fire1 fire
leading true, trailing false1 fire (immediate)1 fire (immediate)
leading true, trailing true1 fire (immediate)2 fires (immediate + trailing)
cancel before wait ends0 fires0 fires
flush while pending1 fire1 fire
maxWaitnot supportedsupported (fired during a 200 ms spam)
Return valuePromiseundefined (sync wrapper)
isPendingyesno (has pending() in full lodash, not this package)

Trailing defaults match. The leading-plus-trailing case does not. perfect-debounce is built around promises: a burst of calls resolves to the last invocation's result (I saw three distinct Promise objects all resolve to 6 after f(1), f(2), f(3) with a ×2 async body). lodash.debounce returns undefined from the wrapper. For UI state libraries that already lean tiny, compare the same size bias in jotai vs zustand and nanostores vs jotai.

Is invoke speed a reason to pick lodash?

Rapid calls (50,000, median of 9, 2 runs)perfect-debouncelodash.debounce
wait 0, µs per call0.303 / 0.2720.066 / 0.103
wait 16, µs per call0.276 / 0.2270.069 / 0.113
create 20,000 wrappers, µs each0.080 / 0.1010.082 / 0.125

lodash wins the micro-benchmark. For a 200 ms search box debounce, wait time dominates by orders of magnitude, so I would not choose on invoke cost. Query-key and fetch coalescing are different tools; see TanStack Query vs SWR if that is the real problem.

Which one should you pick?

SituationMy pick
Async handler, want a Promiseperfect-debounce
Client bundle, size mattersperfect-debounce
Need maxWait so a call cannot starvelodash.debounce
Need leading and trailing both to firelodash.debounce
Already on lodash.debounce with tests for double-firestay

Bottom line: perfect-debounce was 447 B gzip versus 1,163 B for lodash.debounce, with matching trailing-once behavior and a cleaner async API on this box. Who should not switch: code that relies on maxWait or on leading-plus-trailing firing twice. Copy helpers next to debounce often come from the same lodash migration; klona vs structuredClone covers the clone side.

How this was made: I installed both packages on the ShopperCove box, measured esbuild browser bundles, timed create and invoke loops in two order-reversed runs, and checked trailing, leading, cancel, flush, maxWait and promise behavior. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.

Sources

  • https://github.com/unjs/perfect-debounce
  • https://github.com/lodash/lodash
  • https://www.npmjs.com/package/perfect-debounce
  • https://www.npmjs.com/package/lodash.debounce

Related

  • https://www.shoppercove.com/blog/es-toolkit-vs-lodash
  • https://www.shoppercove.com/blog/defu-vs-lodash-merge
  • https://www.shoppercove.com/blog/size-limit-vs-bundlesize
  • https://www.shoppercove.com/blog/jotai-vs-zustand
  • https://www.shoppercove.com/blog/nanostores-vs-jotai
  • https://www.shoppercove.com/blog/tanstack-query-vs-swr
  • https://www.shoppercove.com/blog/klona-vs-structuredclone
  • https://www.shoppercove.com/blog/picocolors-vs-chalk
javascriptdebouncebundle sizeperformanceasynclodashweb performance

Lab evidence

What I found running this

Hands-on on ShopperCove box 6 Oct 2026 ~01:40 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). perfect-debounce 2.1.0 vs lodash.debounce 4.0.8, esbuild 0.28.2 browser ESM minify + gzip -9. Size: perfect-debounce 715/447 B vs lodash.debounce 2,351/1,163 B. Speed (9-round median, order-reversed): create 20,000 wrappers pd 0.080 / 0.101 µs vs lodash 0.082 / 0.125 µs; invoke 50,000 wait0 pd 0.303 / 0.272 µs vs lodash 0.066 / 0.103; wait16 pd 0.276 / 0.227 vs lodash 0.069 / 0.113. Behavior: trailing-once 1/1; L+T lodash 2 vs pd 1; cancel 0/0; flush 1/1; maxWait lodash fires during 200 ms spam; pd promise burst f(1..3) resolves all to 6 with one underlying call; isPending true while waiting. Not tested: browsers, React concurrent, rAF wrappers, full lodash-es import. No affiliate.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 38

    smol-toml vs @iarna/toml: 3x Faster on Deno Cargo

    5 Oct 2026

  • Plate 09

    ohash vs object-hash: 10x Faster, 3.3 KB vs 11 KB

    5 Oct 2026

  • Plate 18

    yaml vs js-yaml: 11x Faster Parse vs Kept Comments

    5 Oct 2026

On this page

  1. What I tested
  2. How much smaller is perfect-debounce?
  3. Do trailing, leading, cancel and flush match?
  4. Is invoke speed a reason to pick lodash?
  5. Which one should you pick?
  6. Sources
  7. Related
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove