Skip to content
Node.js nd libraries 4 min read

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

merge mutates 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.

LodashNative equivalent
_.map, _.filter, _.reduceArray.prototype.map/filter/reduce
_.find, _.includesArray.prototype.find/includes
_.flatten, _.flattenDeepArray.prototype.flat(depth)
_.uniq[...new Set(arr)]
_.cloneDeep (simple data)structuredClone(obj)
_.merge (shallow){ ...a, ...b }
_.pick / _.omitobject destructuring
_.groupByObject.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 ] }

structuredClone covers most deep-clone needs but cannot copy functions or class prototypes — reach for cloneDeep only when your data contains those.

Best practices

  • Import named functions from lodash-es so bundlers can tree-shake unused code.
  • Reach for native map, filter, flat, Set, and structuredClone before adding a Lodash call.
  • Keep cloneDeep, merge, get, debounce, and throttle — they have no clean native replacement.
  • Pass a fresh target object to merge to avoid mutating your sources.
  • Avoid deprecated per-method packages (lodash.clonedeep, lodash.get) — they are unmaintained.
  • Cancel debounce/throttle timers (fn.cancel()) on shutdown to avoid dangling handles.
  • Audit existing Lodash usage periodically; trim helpers that newer Node versions made native.
Last updated June 14, 2026
Was this helpful?