cheat sheet
lodash
Package-level reference for lodash on npm — modular imports, ES2020+ replacements, lodash/fp, debounce/throttle, and the honest 'you might not need this' framing.
lodash
What it is
lodash is the most-downloaded utility library on npm — ~50 million weekly downloads — providing ~300 helpers for arrays, objects, strings, functions, and collections. It was authored by John-David Dalton in 2012 as a faster, modular fork of underscore.js, and became the de-facto utility belt of the entire Node.js ecosystem.
It exposes both a chainable wrapper (_(arr).map(...).filter(...).value()) and standalone functions (_.map(arr, ...)). The core selling points are deep-clone, deep-merge, deep-equal, get/set with path strings, debounce/throttle, groupBy, keyBy, pick/omit, and cloneDeep — utilities that ES2020+ JavaScript still doesn't have built-in, despite many simpler lodash functions now being obsolete.
The honest framing in 2026: half of lodash's surface area is now redundant. Array.prototype.flat, Array.prototype.flatMap, Object.fromEntries, Array.from, optional chaining (?.), nullish coalescing (??), and structuredClone() cover what _.flatten, _.flatMapDeep, _.fromPairs, _.toArray, _.get (sometimes), and _.cloneDeep (sometimes) used to do. But the other half — debounce, throttle, groupBy for older Node, deep-merge with custom resolvers, mapKeys, partition — still has no platform equivalent that matches the ergonomics.
Install
# npm / pnpm / yarn / bun
npm install lodash
pnpm add lodash
yarn add lodash
bun add lodash
Output: runtime dep — lodash ships in your prod bundle. Full UMD bundle is ~25 KB gzipped.
# TypeScript types are NOT bundled — install separately
npm install --save-dev @types/lodash
Output: dev dep — DefinitelyTyped declarations from the community.
# The "fp" variant — auto-curried, data-last, immutable
npm install lodash
import _ from "lodash/fp"; // same package, different entry
Output: functional flavour — _.map(fn, coll) instead of _.map(coll, fn); safer in pipelines and tree-shakes better.
# Per-method packages (one function = one package)
npm install lodash.debounce lodash.throttle lodash.clonedeep
Output: legacy single-purpose packages. Used to be the way to dodge bundle bloat. Mostly obsolete now — modern bundlers tree-shake lodash-es.
# ESM-native fork — strongly preferred for bundlers
npm install lodash-es
Output: same API; published as ESM with sideEffects: false so Webpack/Vite/Rollup tree-shake unused functions. Use this in any bundled app.
Versioning & Node support
- Current major line is
4.x(stable since 2016 — yes, ten years). The API surface has barely changed; lodash is the ultimate "if it ain't broke" library. Many CVEs have been patched; minor versions have shipped throughout 2025. - Pure JS with optional
@types/lodashdeclarations; runs on any modern runtime — Node 14+, Bun, Deno (via npm specifier), Cloudflare Workers, browsers (ES5+ baseline for the main build, ES2015+ forlodash-es). - The main
lodashpackage is CJS by default;lodash-esis ESM. Both publish from the same source. - Always a runtime dependency — your prod code calls the helpers.
- A
5.xline has been on the roadmap for years and has not shipped. Don't wait for it.
Package metadata
- Maintainer: John-David Dalton (
@jdalton) + the lodash org - Project home: github.com/lodash/lodash
- Docs: lodash.com/docs
- npm: npmjs.com/package/lodash
- License: MIT
- First released: April 2012
- Downloads: ~50 million per week — by some counts the most-installed package on npm
Peer dependencies & extras
Lodash is zero-dependency. Common companions:
| Package | Purpose |
|---|---|
@types/lodash | TypeScript declarations (DefinitelyTyped). Required for any TS project using lodash. |
@types/lodash-es | TS declarations for the ESM build. |
lodash-es | ESM-published mirror — preferred for any bundled web app. |
lodash.<method> | Single-method packages (e.g. lodash.debounce). Mostly obsolete; use lodash-es instead. |
lodash/fp | Functional flavour — same package, different entry point. |
babel-plugin-lodash | Rewrites import _ from "lodash" into per-method imports. Largely obsolete now that lodash-es exists. |
Alternatives
| Library | Trade-off |
|---|---|
| Native ES2020+ | Half of lodash is now in the platform — .flat, .flatMap, Object.fromEntries, structuredClone, ?., ??. Free, zero bundle. Pick first. |
| Ramda | Functional-first, auto-curried, data-last (like lodash/fp from day one). Smaller surface area; stricter immutability. Pick for FP-heavy codebases. |
| remeda | TypeScript-first lodash alternative with better type inference. Modular, ESM-native, smaller bundles. Growing fast in 2025-2026. |
| es-toolkit | New ~3KB gzipped lodash alternative from Toss; modern-first, no IE shims. Drop-in for ~30 common helpers. |
| rambdax | Ramda with extras. Same trade-off as Ramda. |
| just-* | The just- family (just-debounce-it, just-clone, just-pick) — single-purpose, zero-dep, modern-only. Pick when you need 2-3 helpers and don't want lodash. |
Common gotchas
- CJS lodash doesn't tree-shake.
import _ from "lodash"in a bundler pulls the entire ~25 KB bundle, even if you only call_.debounce. Uselodash-es(import { debounce } from "lodash-es") or per-method imports (import debounce from "lodash/debounce"). _.get(obj, "a.b.c")is nowobj?.a?.b?.c. Optional chaining covers ~90% of_.getuse cases. Keep_.getfor dynamic path strings only._.cloneDeepis slower thanstructuredClone. NativestructuredClone(obj)(Node 17+, all modern browsers) is 5-10× faster for plain data. lodash still wins for objects with functions, DOM nodes, or class instances — but those rarely round-trip cleanly anyway._.isEqualis structural, not by reference. Two objects with the same shape but different prototypes can compare equal. For class-instance equality, write your own._.mergemutates the first argument._.merge(a, b)modifiesain place and returns it. Use_.merge({}, a, b)to avoid surprising side effects._.debouncekeeps a timer reference — call.cancel()on unmount. In React, an un-cancelled debounce can fire after the component unmounted, triggering state updates on dead components.
Real-world recipes
The patterns that come up in nearly every codebase still using lodash in 2026.
pick / omit / get / set — path-string object surgery
import { pick, omit, get, set } from "lodash-es";
const user = { id: 1, name: "Alice", email: "alice@example.com", password: "secret" };
const publicView = pick(user, ["id", "name", "email"]);
const safeUser = omit(user, ["password"]);
const email = get(user, "email", "no-email@example.com");
const config = {};
set(config, "db.connection.host", "localhost");
set(config, "db.connection.port", 5432);
Output:
publicView = { id: 1, name: "Alice", email: "alice@example.com" }
safeUser = { id: 1, name: "Alice", email: "alice@example.com" }
email = "alice@example.com"
config = { db: { connection: { host: "localhost", port: 5432 } } }
Native equivalents exist but aren't as ergonomic — pick becomes Object.fromEntries(Object.entries(user).filter(([k]) => keys.includes(k))), which is harder to read. lodash's path-string API still wins for dynamic field lists.
Deep clone with cloneDeep — when structuredClone isn't enough
import { cloneDeep } from "lodash-es";
class Money { constructor(public amount: number, public currency: string) {} }
const order = {
id: 42,
total: new Money(99.99, "USD"),
items: [{ sku: "A1" }, { sku: "B2" }],
callback: () => console.log("paid"),
};
const copy = cloneDeep(order);
copy.items[0].sku = "MODIFIED";
console.log(order.items[0].sku); // "A1" — untouched
console.log(copy.total instanceof Money); // true — class preserved
Output: cloneDeep preserves class instances, symbols, and functions; structuredClone throws on functions and class instances. Use structuredClone for plain data, cloneDeep for everything else.
debounce + throttle — rate-limiting callbacks
import { debounce, throttle } from "lodash-es";
// Debounce: fire after `wait` ms of silence
const onSearch = debounce(async (query: string) => {
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
renderResults(results);
}, 300);
// Throttle: fire at most once per `wait` ms
const onScroll = throttle(() => {
updateScrollPosition(window.scrollY);
}, 100);
window.addEventListener("scroll", onScroll);
// In React useEffect cleanup
useEffect(() => {
return () => onSearch.cancel(); // cancel pending invocation
}, []);
Output: debounce waits for the storm to end; throttle samples at intervals. Both are still standout lodash exports — no clean platform equivalent exists.
groupBy + countBy — collection bucketing
import { groupBy, countBy } from "lodash-es";
const orders = [
{ id: 1, status: "shipped", total: 49 },
{ id: 2, status: "pending", total: 99 },
{ id: 3, status: "shipped", total: 19 },
{ id: 4, status: "cancelled", total: 5 },
];
const byStatus = groupBy(orders, "status");
const counts = countBy(orders, "status");
Output:
{
"shipped": [{ "id": 1, "status": "shipped", "total": 49 }, { "id": 3, ... }],
"pending": [{ "id": 2, "status": "pending", "total": 99 }],
"cancelled": [{ "id": 4, "status": "cancelled", "total": 5 }]
}
Object.groupBy shipped in Node 21+ and modern browsers — for new code on supported runtimes, use the native version. countBy has no native equivalent yet.
map + chain — pipeline composition
import _ from "lodash-es";
const result = _.chain(orders)
.filter({ status: "shipped" })
.map((o) => ({ ...o, total: o.total * 1.1 })) // 10% tax
.sumBy("total")
.value();
// lodash/fp equivalent — pipeline-friendly, no .value() needed
import { flow, filter, map, sumBy } from "lodash/fp";
const result2 = flow(
filter({ status: "shipped" }),
map((o: Order) => ({ ...o, total: o.total * 1.1 })),
sumBy("total"),
)(orders);
Output: _.chain is the OO-style pipeline; lodash/fp + flow is the FP-style. The fp flavour tree-shakes better and composes cleanly with TypeScript inference improvements in 2025+.
mapKeys + invert — quick key renames
import { mapKeys, invert, camelCase } from "lodash-es";
const apiResponse = { user_id: 1, full_name: "Alice", created_at: "2026-05-31" };
const camelized = mapKeys(apiResponse, (_v, k) => camelCase(k));
// { userId: 1, fullName: "Alice", createdAt: "2026-05-31" }
const codeToLabel = { US: "United States", GB: "United Kingdom" };
const labelToCode = invert(codeToLabel);
// { "United States": "US", "United Kingdom": "GB" }
Output: still one of the cleanest ways to mass-rename keys in a payload. Native equivalent is Object.fromEntries(Object.entries(...).map(...)) — verbose.
Production deployment
Lodash is a runtime dep; the deployment concerns are bundle size, tree-shaking, and CVE drift.
Pick the right entry point
| Scenario | Import |
|---|---|
| Bundled web app (Vite, Webpack 5+, Rollup, esbuild) | import { debounce } from "lodash-es" — tree-shakes |
| Node-only CLI | import _ from "lodash" — full bundle, fine on the server |
| Tiny browser bundle, 2-3 helpers | import debounce from "just-debounce-it" (or es-toolkit, remeda) |
| TypeScript project | Always npm install --save-dev @types/lodash (or @types/lodash-es) |
Bundle size measurements
| Import form | Result (gzipped) |
|---|---|
import _ from "lodash" | ~25 KB |
import _ from "lodash-es" with tree-shaking | only what you import — ~2 KB per helper |
import debounce from "lodash/debounce" | ~3 KB |
import { debounce } from "lodash.debounce" | ~2 KB |
Native Object.groupBy(...) etc. | 0 KB |
CVE drift
lodash has had a steady trickle of CVEs (zipObjectDeep, set, merge prototype-pollution chains; see Security below). Dependabot keeps the patch line current — pin ^4.17.21 or newer. Don't trust any lodash older than 2021.
Performance tuning
Native first
// Slow: lodash deep equal across 10k items
arr.filter((a) => _.isEqual(a, target)); // ~500ms
// Fast: native + structural comparison via JSON.stringify (or stable-stringify)
const targetKey = JSON.stringify(target);
arr.filter((a) => JSON.stringify(a) === targetKey); // ~50ms
For hot loops, native operators are 5-50× faster than lodash. Reach for lodash for readability, not performance.
cloneDeep is expensive
cloneDeep on a 100 KB object can take 5-20ms. Avoid in hot paths; freeze the original and pass references where possible. structuredClone is 5-10× faster but doesn't handle functions.
chain is slower than flow
_.chain(arr).map(...).filter(...).value() allocates a wrapper object plus a closure per step. lodash/fp + flow is materially faster and tree-shakes — prefer it for new code.
Memoise expensive selectors
import { memoize } from "lodash-es";
const expensiveFn = memoize((input: string) => doSlowThing(input));
_.memoize keys on the first argument by default (use .cache.set() or pass a resolver). Fine for pure functions; broken for functions with multiple meaningful arguments — pass an explicit resolver.
Version migration guide
Lodash 4.x has been stable since 2016. There's no live "v5" to migrate to — but several quality-of-life migrations are worth doing.
lodash → lodash-es (recommended for bundled apps)
// Before
import _ from "lodash";
const debounced = _.debounce(fn, 300);
// After
import { debounce } from "lodash-es";
const debounced = debounce(fn, 300);
Output: same runtime behaviour; bundler tree-shakes unused helpers. Saves 10-20 KB gzipped typically.
lodash → native ES2020+ (recommended where possible)
| lodash | Native replacement |
|---|---|
_.get(obj, "a.b.c") | obj?.a?.b?.c |
_.isNil(x) | x == null |
_.flatten(arr) | arr.flat() |
_.flatMap(arr, fn) | arr.flatMap(fn) |
_.fromPairs(entries) | Object.fromEntries(entries) |
_.toPairs(obj) | Object.entries(obj) |
_.cloneDeep(obj) (plain data) | structuredClone(obj) |
_.groupBy(arr, "key") | Object.groupBy(arr, x => x.key) (Node 21+) |
_.uniq(arr) | [...new Set(arr)] |
Keep debounce, throttle, cloneDeep (for non-plain data), pick/omit (dynamic keys), mapKeys, partition, keyBy, countBy.
lodash/fp migration
// OO style — chainable
_.chain(arr).map(fn).filter(pred).value();
// FP style — composable
import { flow, map, filter } from "lodash/fp";
flow(map(fn), filter(pred))(arr);
lodash/fp is auto-curried and data-last — composes cleanly into pipelines. Tree-shakes better than chain.
Security considerations
Lodash's CVE history is real — almost every one is prototype-pollution via deep-set helpers.
| CVE | Function | Year | Fixed in |
|---|---|---|---|
| CVE-2018-16487 | defaultsDeep, merge | 2018 | 4.17.11 |
| CVE-2019-10744 | defaultsDeep | 2019 | 4.17.12 |
| CVE-2020-8203 | zipObjectDeep, set, setWith | 2020 | 4.17.19 |
| CVE-2021-23337 | template | 2021 | 4.17.21 |
| Various | set, setWith | 2020-2022 | 4.17.21+ |
Rules:
- Pin
lodashto^4.17.21or newer. Older versions are exploitable via prototype-pollution if you pass user input to_.set,_.merge,_.defaultsDeep, or_.zipObjectDeep. - Never
_.merge(target, userInput)or_.set(target, userPath, val). Even on the patched version, design the data flow so user input never feeds path strings or deep-merge targets. _.templateiseval. It executes user-provided template strings. Don't use with untrusted input.- Audit transitive deps. Many older libraries pin
lodash@^4.17.5(pre-patch).npm auditand Dependabot catch these; bump or yarn resolutions/npm overrides as needed.
Testing & CI integration
import { describe, it, expect } from "vitest";
import { groupBy, cloneDeep } from "lodash-es";
describe("groupBy", () => {
it("buckets by key string", () => {
const result = groupBy([{ t: "a" }, { t: "b" }, { t: "a" }], "t");
expect(result).toEqual({ a: [{ t: "a" }, { t: "a" }], b: [{ t: "b" }] });
});
});
describe("cloneDeep", () => {
it("preserves nested structure", () => {
const src = { a: { b: { c: 1 } } };
const copy = cloneDeep(src);
copy.a.b.c = 99;
expect(src.a.b.c).toBe(1);
});
});
Output: lodash functions are pure (mostly) — easy to unit-test. Test your wrappers, not lodash itself.
For CI:
# .github/workflows/ci.yml
- run: npm audit --production --audit-level=high
Catches new lodash CVEs before they ship.
Ecosystem integrations
| Tool | Integration |
|---|---|
react | debounce/throttle in event handlers; cloneDeep for state snapshots |
redux | cloneDeep historically; modern code uses Immer instead |
webpack / vite | Use lodash-es for tree-shaking |
babel-plugin-lodash | Auto-rewrites import _ from "lodash" into per-method imports (mostly obsolete) |
babel-plugin-transform-imports | Same idea, more general |
eslint-plugin-lodash-fp | Lints lodash/fp usage for purity violations |
@types/lodash | TypeScript declarations |
Troubleshooting common errors
Module not found: 'lodash-es'— install separately (npm install lodash-es). It's not inlodash.Cannot find module 'lodash/debounce'— TypeScript compilation. Install@types/lodash.- Bundle size explodes after adding lodash — using CJS
lodashin a bundler. Switch tolodash-esor per-method imports. _.mergeis mutating my object — that's the documented behaviour. Use_.merge({}, a, b).- debounce never fires in React — created inside a render. Use
useMemo(() => debounce(fn, 300), [])oruseRef. _.isEqualreturns false on what looks identical — class instances with different prototypes, or one has aSymbolkey. CheckgetPrototypeOf/Reflect.ownKeys.- Audit warns about prototype pollution — upgrade to
^4.17.21or newer; switch tolodash-esif not already.
When NOT to use this
- You're starting a new project in 2026. Reach for native ES2020+ first. Only add lodash when you hit a concrete need (
debounce,cloneDeepfor non-plain data,groupByon older Node, dynamic-pathget/set). - Bundle-size-critical web app. Use
es-toolkit(~3KB) orremeda(~5KB) instead. Both cover ~30 of the most-common lodash helpers with better TypeScript inference. - You only need 1-2 helpers.
npm install just-debounce-itornpm install just-clone— single-purpose, zero-dep, ~1KB each. - You're chaining with Ramda or rxjs. Use their pipe operators; mixing paradigms hurts readability.
- Plain
JSON.parse(JSON.stringify(x))is enough. For trusted plain data, the JSON round-trip clones in one line and outperformscloneDeep. (Still slower thanstructuredClonefor plain data, but no dependency required.) - Modern React + Redux Toolkit. RTK uses Immer for immutable updates — don't pile on lodash too.
See also
- JavaScript: array methods — native replacements for many lodash helpers
- Concept: async — debounce/throttle in async contexts
- Concept: JSON — cloning and merging at API boundaries
- Packages: npm-dayjs — date helpers; lodash has no date module