Plate 28
perfect-debounce vs lodash.debounce: 447 B vs 1.2 KB
Aditya Challa5 min read
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) | Minified | gzip -9 |
|---|---|---|
| perfect-debounce | 715 B | 447 B |
| lodash.debounce | 2,351 B | 1,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?
| Case | perfect-debounce 2.1.0 | lodash.debounce 4.0.8 |
|---|---|---|
| 5 rapid calls, trailing default | 1 fire | 1 fire |
| leading true, trailing false | 1 fire (immediate) | 1 fire (immediate) |
| leading true, trailing true | 1 fire (immediate) | 2 fires (immediate + trailing) |
| cancel before wait ends | 0 fires | 0 fires |
| flush while pending | 1 fire | 1 fire |
| maxWait | not supported | supported (fired during a 200 ms spam) |
| Return value | Promise | undefined (sync wrapper) |
| isPending | yes | no (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-debounce | lodash.debounce |
|---|---|---|
| wait 0, µs per call | 0.303 / 0.272 | 0.066 / 0.103 |
| wait 16, µs per call | 0.276 / 0.227 | 0.069 / 0.113 |
| create 20,000 wrappers, µs each | 0.080 / 0.101 | 0.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?
| Situation | My pick |
|---|---|
| Async handler, want a Promise | perfect-debounce |
| Client bundle, size matters | perfect-debounce |
| Need maxWait so a call cannot starve | lodash.debounce |
| Need leading and trailing both to fire | lodash.debounce |
| Already on lodash.debounce with tests for double-fire | stay |
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
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.