Plate 68
Defu vs lodash.merge: 6x Faster, 452 B vs 4 KB
Aditya Challa5 min read
I timed nested config merges on this box: defu 6.1.7 finished about 430,000–475,000 merges per second, while lodash.merge 4.6.2 managed about 75,000–77,000. That is roughly 5.5–6× faster, for 452 B gzip instead of 4.1 KB.
Short answer: use defu when you want a tiny defaults helper that leaves your defaults object alone and skips null/undefined overlays. Reach for lodash.merge only if you already ship lodash, or you rely on its index-by-index array merge and null-overwrites-default rules. 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:45 IST.
- Versions: defu 6.1.7, lodash.merge 4.6.2, Node 22.20.0, esbuild 0.28.2 browser ESM minify + gzip -9.
- Fixture: /workspace/bench-defu-lodash-merge/. Nested config (server/build/features/meta) merged 50,000 times per round, 9 rounds, median, after warm-up. I ran it twice with the order reversed. Shallow Object.assign and object-spread were baselines only.
- Findings: defu 427,847 and 475,272 ops/s; lodash.merge 77,085 and 75,387 ops/s. Bundle: defu 827 B minified / 452 B gzip; lodash.merge 10,647 B / 4,119 B. defu left the defaults object unchanged; lodash.merge mutated its first argument.
- One surprise: defu concatenates arrays by default (overlay tags ["c"] plus defaults ["a","b"] became ["c","a","b"]), while lodash.merge merged by index (["c","b"]). null in the overlay was ignored by defu and applied by lodash.merge.
- Not tested: full lodash (only the lodash.merge package), defuFn customizers beyond a replace-array helper, circular objects, class instances, browsers, Bun or Deno.
How much smaller is defu?
| Build (esbuild browser minify) | Minified | gzip -9 |
|---|---|---|
| defu 6.1.7 | 827 B | 452 B |
| lodash.merge 4.6.2 | 10,647 B | 4,119 B |
About 9× smaller on gzip. If you already carry lodash for other helpers, the merge export is effectively free and this table does not apply. For a single utility, size-limit vs bundlesize will flag the 4 KB. Broader lodash trimming is covered in es-toolkit vs lodash.
Is defu faster than lodash.merge?
| Nested config merge (median of 9) | defu | lodash.merge | Object.assign shallow | spread shallow |
|---|---|---|---|---|
| run 1 (defu first) | 427,847 ops/s | 77,085 ops/s | 9.44M ops/s | 13.4M ops/s |
| run 2 (order reversed) | 475,272 ops/s | 75,387 ops/s | 8.85M ops/s | 11.7M ops/s |
Yes, by about 5.5–6× on this fixture. Shallow assign/spread stay in the millions of ops/s because they do not walk nested keys; they are not a substitute when you need deep defaults. I used a Nuxt-ish config shape (server, build, features, meta) so the walk depth matches what Vite/Nuxt plugins actually merge.
Do arrays and null behave the same?
| Case | defu | lodash.merge |
|---|---|---|
| Overlay { tags: ["c"] } on defaults { tags: ["a","b"] } | ["c","a","b"] (concat) | ["c","b"] (index merge) |
| Overlay { a: null } on { a: 1, b: 2 } | { a: 1, b: 2 } (null skipped) | { a: null, b: 2 } |
| Overlay { a: undefined } | defaults kept | defaults kept |
| Nested keep of sibling keys | kept | kept |
| Defaults object after call | unchanged | first arg mutated |
These rules matter more than the speed gap. If your config uses arrays as replaceable lists (plugin lists, tag lists), default defu will grow them. createDefu can replace arrays instead; I verified that custom path returned ["c"] only. For schema-level defaults with clearer types, compare notes with Zod vs Valibot.
Which one should you pick?
| Situation | My pick |
|---|---|
| Nuxt / unjs-style defaults, tiny bundle | defu |
| Need null to clear a default | lodash.merge |
| Arrays should replace, not grow | createDefu replace rule, or lodash with care |
| Already ship lodash | lodash.merge |
| Shallow one-level options only | Object.assign / spread |
Bottom line: defu was about 6× faster and about 9× smaller than lodash.merge on this box, and it does not mutate your defaults. Who should not switch: code that depends on lodash.merge writing nulls into the result or merging array slots by index. Drop unused deps with knip vs depcheck, and keep date helpers honest with dayjs vs date-fns if those sit next to your config layer.
How this was made: I installed defu and lodash.merge on the ShopperCove box, measured esbuild minify+gzip sizes, timed nested merges in two order-reversed runs, checked array/null/mutation behavior (including a createDefu array-replace helper), and saved the JSON results. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/unjs/defu
- https://www.npmjs.com/package/defu
- https://www.npmjs.com/package/lodash.merge
- https://lodash.com/docs/#merge
Related
- https://www.shoppercove.com/blog/es-toolkit-vs-lodash
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/zod-vs-valibot
- https://www.shoppercove.com/blog/dayjs-vs-date-fns
- https://www.shoppercove.com/blog/picocolors-vs-chalk
- https://www.shoppercove.com/blog/ncu-vs-npm-outdated
- https://www.shoppercove.com/blog/tinyexec-vs-execa
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~01:45 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). defu 6.1.7 vs lodash.merge 4.6.2, esbuild 0.28.2 browser ESM minify + gzip -9. Fixture /workspace/bench-defu-lodash-merge/. Size: defu 827/452 B vs lodash.merge 10,647/4,119 B. Speed (50k nested merges, 9-round median, order-reversed runs): defu 427,847 / 475,272 ops/s; lodash.merge 77,085 / 75,387; Object.assign 9.44M / 8.85M; spread 13.4M / 11.7M. Behavior (res-behavior.json): defu concat arrays ["c","a","b"] vs lodash index-merge ["c","b"]; defu skips null (keeps default 1) while lodash writes null; both skip undefined; defu defaults unchanged; lodash.merge mutates target; createDefu replace-array returned ["c"] only. Raw: res-bundle.json, res-speed-run1.json, res-speed-run2.json, res-behavior.json. Not tested: full lodash, circular objects, class instances, browsers, Bun/Deno. No affiliate.