Lodash & Utility Libraries
Lodash is the most widely used utility library in the JavaScript ecosystem, providing battle-tested helpers for transforming arrays, objects, and collections. While modern JavaScript has absorbed many of its features as native methods, Lodash still shines for operations that remain awkward in plain JS — deep cloning, deep merging, safe nested property access, and rate-limiting functions. Using it well today means knowing which helpers are still worth importing and how to keep them out of your bundle when you don’t need them.
Installing Lodash
Lodash ships in two flavors. The classic lodash package is CommonJS-first, while lodash-es exposes ES modules that bundlers can tree-shake. On the server, either works; for code shared with a frontend, prefer lodash-es.
npm install lodash
# or, for tree-shakeable ES modules
npm install lodash-es
// ES modules (Node 20/22)
import _ from 'lodash';
import { cloneDeep, groupBy } from 'lodash-es';
// CommonJS
const _ = require('lodash');
Working with objects
The object helpers are the strongest argument for keeping Lodash around. cloneDeep produces a fully independent copy of nested structures, merge recursively combines objects, and get safely reads deep paths without throwing on undefined.
import { cloneDeep, merge, get } from 'lodash-es';
const config = { server: { host: 'localhost', ports: [3000] } };
const copy = cloneDeep(config);
copy.server.ports.push(8080);
console.log(config.server.ports); // original untouched
const defaults = { server: { host: '0.0.0.0', timeout: 5000 } };
const merged = merge({}, defaults, config);
console.log(merged.server);
const host = get(config, 'server.host', 'fallback');
const missing = get(config, 'server.tls.cert', 'no-cert');
console.log(host, missing);
Output:
[ 3000 ]
{ host: 'localhost', timeout: 5000, ports: [ 3000 ] }
localhost no-cert
mergemutates its first argument. Always pass a fresh{}as the target so your source objects stay intact.
Working with collections
groupBy partitions a collection into buckets keyed by the result of an iteratee — a common need when shaping query results for an API response.
import { groupBy, orderBy, keyBy } from 'lodash-es';
const orders = [
{ id: 1, status: 'paid', total: 40 },
{ id: 2, status: 'pending', total: 12 },
{ id: 3, status: 'paid', total: 25 },
];
const byStatus = groupBy(orders, 'status');
console.log(Object.keys(byStatus));
const sorted = orderBy(orders, ['total'], ['desc']);
console.log(sorted.map((o) => o.id));
const byId = keyBy(orders, 'id');
console.log(byId[2].status);
Output:
[ 'paid', 'pending' ]
[ 1, 3, 2 ]
pending
Rate-limiting with debounce and throttle
debounce and throttle wrap a function so it fires less often — invaluable for expensive handlers like search-as-you-type, file watchers, or flushing buffered writes. debounce waits until activity stops; throttle guarantees at most one call per interval.
import { debounce, throttle } from 'lodash-es';
const saveDraft = debounce((text) => {
console.log('saved:', text);
}, 300);
saveDraft('h');
saveDraft('he');
saveDraft('hello'); // only this one fires, 300ms after the last call
const reportProgress = throttle((pct) => {
console.log('progress:', pct);
}, 1000);
Output:
saved: hello
Tree-shaking and bundle size
Importing the default lodash build pulls the entire library. With lodash-es and a bundler such as esbuild, Rollup, or Vite, named imports let dead-code elimination drop everything you don’t reference. Avoid import _ from 'lodash-es' (the whole namespace) and per-method packages like lodash.clonedeep, which are deprecated and no longer maintained.
// Tree-shakeable — only cloneDeep ends up in the bundle
import { cloneDeep } from 'lodash-es';
// NOT tree-shakeable — pulls the full library
import _ from 'lodash';
Which utilities are now native
A large slice of Lodash predates modern JavaScript. For these, prefer the built-in — fewer dependencies and no bundle cost.
| Lodash | Native equivalent |
|---|---|
_.map, _.filter, _.reduce | Array.prototype.map/filter/reduce |
_.find, _.includes | Array.prototype.find/includes |
_.flatten, _.flattenDeep | Array.prototype.flat(depth) |
_.uniq | [...new Set(arr)] |
_.cloneDeep (simple data) | structuredClone(obj) |
_.merge (shallow) | { ...a, ...b } |
_.pick / _.omit | object destructuring |
_.groupBy | Object.groupBy (Node 21+) |
// structuredClone handles dates, maps, sets, typed arrays
const copy = structuredClone({ when: new Date(), tags: new Set(['a']) });
// Object.groupBy is native in Node 21+
const grouped = Object.groupBy([1, 2, 3, 4], (n) => (n % 2 ? 'odd' : 'even'));
console.log(grouped);
Output:
{ odd: [ 1, 3 ], even: [ 2, 4 ] }
structuredClonecovers most deep-clone needs but cannot copy functions or class prototypes — reach forcloneDeeponly when your data contains those.
Best practices
- Import named functions from
lodash-esso bundlers can tree-shake unused code. - Reach for native
map,filter,flat,Set, andstructuredClonebefore adding a Lodash call. - Keep
cloneDeep,merge,get,debounce, andthrottle— they have no clean native replacement. - Pass a fresh target object to
mergeto avoid mutating your sources. - Avoid deprecated per-method packages (
lodash.clonedeep,lodash.get) — they are unmaintained. - Cancel
debounce/throttletimers (fn.cancel()) on shutdown to avoid dangling handles. - Audit existing Lodash usage periodically; trim helpers that newer Node versions made native.