TL;DR
Lodash is still the most downloaded package in the npm ecosystem (~45M weekly downloads) but modern JavaScript has replaced many of its functions natively. Radash is a TypeScript-native, async-aware alternative to Lodash that covers gaps native JS still has. In 2026, the right answer is often: use native JavaScript first, reach for Radash when you need async utilities or typed helpers, and use Lodash only if you're on a legacy codebase that already depends on it.
Key Takeaways
- lodash: ~45M weekly downloads — comprehensive, tree-shakable, but many functions now native
- radash: ~400K weekly downloads — TypeScript-native, async-aware, modern replacement
- native JS: 0KB —
Object.groupBy,Array.toSorted,structuredClone,Array.findLastcover dozens of Lodash functions - Most projects in 2026 can remove Lodash entirely with modern JS and Radash for async gaps
- Lodash's
_.groupBy=Object.groupBy(ES2024),_.cloneDeep=structuredClone(Node 17+) - Radash's
tryit,mapLimit,retryfill genuine gaps native JS doesn't cover
Download Trends
| Package | Weekly Downloads | TypeScript | Async | Bundle Size |
|---|---|---|---|---|
lodash | ~45M | ✅ @types | ❌ | ~70KB full, per-fn minimal |
radash | ~400K | ✅ Native | ✅ | ~25KB |
| native JS | N/A | ✅ | ✅ Promises | 0KB |
What Native JavaScript Covers Now
Before reaching for a utility library, check if native JS already handles your use case:
// ❌ Old: lodash
import { groupBy, cloneDeep, sortBy, uniq, chunk, flatten } from "lodash"
// ✅ New: Native JS (ES2024+)
// _.groupBy → Object.groupBy (ES2024)
const packages = [
{ name: "react", category: "ui" },
{ name: "vue", category: "ui" },
{ name: "express", category: "server" },
]
const grouped = Object.groupBy(packages, (p) => p.category)
// { ui: [{...}, {...}], server: [{...}] }
// _.cloneDeep → structuredClone (Node 17+, browsers 2022+)
const original = { data: { nested: { value: 42 } } }
const copy = structuredClone(original)
copy.data.nested.value = 99
console.log(original.data.nested.value) // 42 — deep clone
// _.sortBy → Array.toSorted (non-mutating, ES2023)
const sorted = packages.toSorted((a, b) => a.name.localeCompare(b.name))
// _.findLast → Array.findLast (ES2023)
const lastUi = packages.findLast((p) => p.category === "ui")
// _.uniq → new Set([...])
const downloads = [100, 200, 100, 300, 200]
const unique = [...new Set(downloads)] // [100, 200, 300]
// _.flatten (1 level) → Array.flat()
const nested = [[1, 2], [3, 4], [5]]
const flat = nested.flat() // [1, 2, 3, 4, 5]
// _.flatMap → Array.flatMap()
const packages2 = [{ name: "react", tags: ["ui", "popular"] }, { name: "lodash", tags: ["utils"] }]
const allTags = packages2.flatMap((p) => p.tags) // ["ui", "popular", "utils"]
// _.chunk → native (no built-in, but trivial to implement)
function chunk<T>(arr: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
)
}
// _.omit / _.pick → native destructuring
const { id, __v, ...rest } = dbDocument // "omit" id and __v
const { name, email } = user // "pick" name and email
// _.merge → structuredClone + Object.assign (for most cases)
const defaults = { timeout: 5000, retries: 3, verbose: false }
const config = { ...defaults, ...userConfig } // shallow merge
Native replacements cheat sheet
| Lodash | Native Alternative | Node.js / ES Version |
|---|---|---|
_.cloneDeep | structuredClone | Node 17+ / ES2022 |
_.groupBy | Object.groupBy | Node 21+ / ES2024 |
_.sortBy | Array.toSorted | Node 20+ / ES2023 |
_.findLast | Array.findLast | Node 18+ / ES2023 |
_.flatten | Array.flat | Node 11+ / ES2019 |
_.flatMap | Array.flatMap | Node 11+ / ES2019 |
_.uniq | [...new Set(arr)] | Always |
_.merge | { ...a, ...b } | Always (shallow) |
_.pick / _.omit | Destructuring | Always |
_.isEmpty | obj && Object.keys(obj).length === 0 | Always |
_.isNil | value == null | Always |
_.isArray | Array.isArray | Always |
_.capitalize | str[0].toUpperCase() + str.slice(1) | Always |
Lodash
Lodash remains the most installed npm package — largely due to transitive dependencies and legacy codebases. When you do need it, use per-function imports to avoid bundling the full 70KB:
Tree-shaking with per-function imports
// ❌ Bundles entire Lodash (~70KB gzipped ~24KB)
import _ from "lodash"
const result = _.merge({}, defaults, config)
// ✅ Only imports the merge function (~1KB)
import merge from "lodash/merge"
const result = merge({}, defaults, config)
// ✅ Or named imports (same as above with tree-shaking bundlers)
import { merge, debounce, throttle } from "lodash-es" // ESM version
Where Lodash still shines
import debounce from "lodash/debounce"
import throttle from "lodash/throttle"
import memoize from "lodash/memoize"
import merge from "lodash/merge"
// Deep merge (structuredClone is clone-only, not merge):
const config = merge({}, defaultConfig, environmentConfig, userConfig)
// Lodash merge recursively merges nested objects — spread only does shallow
// Debounce (no native equivalent):
const searchPackages = debounce(async (query: string) => {
const results = await fetchPackages(query)
setResults(results)
}, 300)
// Throttle API calls:
const logActivity = throttle((event: string) => {
analytics.track(event)
}, 1000) // Max once per second
// Memoize expensive calculations:
const getPackageScore = memoize((packageData: PackageData): number => {
// expensive computation
return calculateHealthScore(packageData)
})
// Template literals (Lodash's _.template for dynamic strings):
import template from "lodash/template"
const compiled = template("Hello <%= name %>! Your package <%= pkg %> has <%= downloads %> downloads.")
const message = compiled({ name: "Royce", pkg: "react", downloads: "25M" })
Functions Lodash still handles better than native
import get from "lodash/get"
import set from "lodash/set"
import has from "lodash/has"
import cloneDeepWith from "lodash/cloneDeepWith"
// Safe deep property access with default (before optional chaining + nullish coalescing):
const city = get(user, "address.city", "Unknown") // Now: user?.address?.city ?? "Unknown"
// Deep set (mutates — useful for dynamic paths):
const path = "settings.notifications.email"
set(config, path, true)
// Custom deep clone (e.g., omit certain types):
const cloned = cloneDeepWith(obj, (value) => {
if (value instanceof Date) return new Date(value.getTime())
// Return undefined to use default cloning for other types
})
// _.mergeWith for custom merge logic:
import mergeWith from "lodash/mergeWith"
const merged = mergeWith({}, base, override, (objValue, srcValue) => {
if (Array.isArray(objValue)) return objValue.concat(srcValue) // Concat arrays instead of replace
})
Radash
Radash is the modern Lodash — built for TypeScript, covers async patterns, and ships as ESM with zero dependencies.
Async utilities (Radash's killer feature)
import { map, mapLimit, parallel, tryit, retry, sleep } from "radash"
// map: typed async map
const packageStats = await map(["react", "vue", "angular"], async (name) => {
const stats = await fetchPackageStats(name)
return { name, ...stats }
})
// mapLimit: concurrency-limited async map (like p-limit but built-in)
const results = await mapLimit(
packageNames, // Array of items
5, // Max 5 concurrent
async (name) => fetchPackageData(name)
)
// parallel: object version — like Promise.all for named results
const { user, packages, stats } = await parallel({
user: fetchUser(userId),
packages: fetchUserPackages(userId),
stats: fetchUsageStats(userId),
})
// All three fetches run concurrently, result is fully typed
// tryit: error handling without try/catch
const [error, data] = await tryit(fetchPackageData)("react")
if (error) {
console.error("Failed:", error.message)
} else {
console.log(data.downloads)
}
// retry: retry with backoff
const data = await retry(
{ times: 3, delay: 1000 }, // 3 retries, 1s delay
async () => {
const response = await fetch("https://api.pkgpulse.com/packages")
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
)
// sleep: typed promise-based sleep
await sleep(500) // Pause 500ms (vs new Promise(r => setTimeout(r, 500)))
Type-safe object utilities
import { pick, omit, shake, crush, construct } from "radash"
// pick: typed subset (Lodash _.pick but TypeScript-native)
const userInput = pick(formData, ["name", "email", "role"])
// TypeScript infers the return type as { name: string; email: string; role: string }
// omit: typed exclusion
const publicUser = omit(user, ["passwordHash", "internalId"])
// shake: remove null/undefined/falsy values
const cleaned = shake({ name: "react", author: null, version: "18.0.0", deprecated: undefined })
// { name: "react", version: "18.0.0" }
// crush: flatten nested object to dot-notation keys
const flat = crush({ user: { name: "Royce", address: { city: "NYC" } } })
// { "user.name": "Royce", "user.address.city": "NYC" }
// construct: reverse of crush
const nested = construct({ "user.name": "Royce", "user.address.city": "NYC" })
// { user: { name: "Royce", address: { city: "NYC" } } }
Array utilities
import { group, cluster, unique, diff, intersect, zip, first, last, flat } from "radash"
// group: like Object.groupBy but TypeScript-native
const byCategory = group(packages, (p) => p.category)
// Map<string, Package[]> — typed, handles missing keys
// cluster: chunk into balanced groups (more flexible than _.chunk)
const batches = cluster(packageNames, 10) // Groups of 10
// unique: by identity or key
const uniqueByName = unique(packages, (p) => p.name)
// diff: array set difference
const removed = diff(previousPackages, currentPackages, (p) => p.id)
// zip: typed tuple zip
const pairs = zip(["react", "vue"], [25000000, 8000000])
// [["react", 25000000], ["vue", 8000000]]
String and number utilities
import { pascal, camel, snake, title, trim, uid } from "radash"
// String case conversion (TypeScript-safe):
pascal("hello world") // "HelloWorld"
camel("hello-world") // "helloWorld"
snake("helloWorld") // "hello_world"
title("npm package") // "Npm Package"
// Generate unique IDs:
const id = uid(16) // 16-char random alphanumeric
const longId = uid(32, "0123456789abcdef") // Hex ID
Feature Comparison
| Feature | Lodash | Radash | Native JS |
|---|---|---|---|
| Weekly downloads | ~45M | ~400K | N/A |
| TypeScript | ✅ @types/lodash | ✅ Native | ✅ |
| Async utilities | ❌ | ✅ mapLimit, retry, parallel | ✅ Promises |
| Bundle size | ~70KB (0 if tree-shaken) | ~25KB | 0KB |
| ESM support | ✅ lodash-es | ✅ | ✅ |
| Deep merge | ✅ Excellent | ❌ | ❌ |
| Debounce/throttle | ✅ | ❌ | ❌ |
| Deep clone | ✅ cloneDeepWith | ❌ | ✅ structuredClone |
| groupBy | ✅ | ✅ group | ✅ Object.groupBy |
| Concurrency control | ❌ | ✅ mapLimit | ❌ |
| Error handling | ❌ | ✅ tryit | ✅ try/catch |
| Dependencies | 0 | 0 | 0 |
| Last major release | 2021 (v4) | Active 2024 | Spec-driven |
When to Use Each
Stick with native JavaScript if:
- You need
groupBy,findLast,flat,flatMap,toSorted— all native now - You're doing shallow merges (
{ ...a, ...b }is fine) - Your runtime is Node.js 18+ / modern browsers (everything ES2023+ is available)
- Bundle size is critical
Choose Radash if:
- You need async utilities:
mapLimit,retry,parallel,tryit - TypeScript inference matters — Radash's types are excellent
- You need
pick/omitwith proper type narrowing - Building a new project and want a modern alternative to Lodash
Keep Lodash if:
- Legacy codebase already uses it (don't refactor what isn't broken)
- You need
_.mergefor deep recursive object merging - You need
debounce/throttleand don't want a separate package - You need
_.templatefor string interpolation - Radash doesn't cover your specific edge case
Migration: Replacing Lodash with Native + Radash
// Before (Lodash):
import { groupBy, cloneDeep, sortBy, uniq, merge, debounce, mapSeries } from "lodash"
// After:
// groupBy → native
const grouped = Object.groupBy(items, (i) => i.category)
// cloneDeep → native
const copy = structuredClone(obj)
// sortBy → native
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name))
// uniq → native
const unique = [...new Set(ids)]
// merge → still use lodash/merge (no native equivalent for deep merge)
import merge from "lodash/merge"
// debounce → still use lodash/debounce or write your own
import debounce from "lodash/debounce"
// mapSeries (sequential async map) → radash
import { map } from "radash" // radash map is sequential by default
// Or use mapLimit for controlled concurrency:
import { mapLimit } from "radash"
await mapLimit(items, 1, processor) // limit=1 is sequential
Bundle Size and Tree-Shaking in Modern Toolchains
Bundle size is one area where the choice between these libraries has tangible impact on user experience. Lodash's 70KB minified size is often cited as a concern, but with Vite, webpack 5, or Rollup, properly configured tree-shaking reduces that to only the functions you actually import. If you import debounce from lodash-es, you bundle roughly 1KB — not 70KB. The problem is that tree-shaking requires using lodash-es (the ESM build) rather than the CommonJS default lodash package, and it requires that your bundler is configured to handle side-effect-free modules. Many older build configurations still bundle the entire Lodash because of incorrect configuration or CJS imports. Radash ships as ESM natively with zero CommonJS compatibility shims, making tree-shaking reliable by default — whatever you import is what you bundle, nothing more. Native JavaScript has zero bundle cost, but the polyfill story matters for browser targets: Object.groupBy requires Safari 17.4+ and is not available in Chrome before 117, which may require a polyfill for applications targeting older browsers.
Async Patterns and Error Handling Philosophy
Radash's approach to async error handling via tryit represents a deliberate design choice that aligns with the Go/Rust pattern of explicit error handling over exceptions. The [error, data] tuple return eliminates the try-catch overhead and makes error handling a first-class concern in the function signature. This has an interesting effect on code reviews: functions that use tryit make it immediately visible whether error cases are handled, whereas try-catch blocks can be silently omitted or empty-caught. The Radash retry function fills a genuine gap — production applications need retry logic with exponential backoff for network requests, database connections, and third-party API calls, and implementing this correctly from scratch (handling promise rejection, computing delay intervals, respecting maximum attempts) is surprisingly error-prone. Using retry standardizes the pattern across a codebase. For more advanced retry scenarios — like respecting the Retry-After header from rate-limited APIs — combining Radash's retry with custom delay logic provides a clean foundation.
Migration Strategy: Removing Lodash Incrementally
Large codebases that want to reduce Lodash dependency can do so incrementally without a big-bang rewrite. The recommended approach is to run a Lodash usage audit (grep -r "from 'lodash'" src/) to identify which functions are actually used, then classify each into three buckets: already-native (replace with native JS immediately), covered by Radash (replace with Radash equivalents over time), and Lodash-only (keep the specific Lodash import). The second bucket — functions like _.debounce, _.throttle, and _.merge — should not be replaced with naive implementations, since Lodash's implementations handle edge cases (immediate invocation, leading/trailing edge timing, circular reference safety) that simple implementations miss. Radash does not cover debounce or throttle; for those, keeping lodash/debounce as a direct import while removing the rest is perfectly acceptable. ESLint rules can enforce that import _ from 'lodash' is banned in favor of per-function imports, guiding the migration gradually without requiring a dedicated refactoring sprint.
Community and Long-Term Maintenance
Lodash's maintenance situation is the most discussed concern in the JavaScript community. Lodash v4 was released in 2016, and while the library remains widely used and receives security patches, no v5 has shipped despite years of development in the lodash/lodash v5 branch on GitHub. The practical question is whether the lack of major releases matters — for a utility library, stability is arguably a feature, and the functions in Lodash v4 are correct and battle-tested. The concern is ecosystem abandonment risk: if the maintainers move on and a security vulnerability is discovered, the response time is uncertain. Radash is actively maintained (last release in late 2024) but has a smaller contributor base. Native JavaScript functions are maintained by browser vendors and TC39 — the most reliable long-term bet — but the spec iteration cycle means gaps remain in 2026. The safest architecture is to minimize library dependencies by defaulting to native JavaScript, using Radash for the async and functional utilities that native JS genuinely lacks, and maintaining targeted Lodash imports only for functions with no suitable replacement.
Practical Decision Framework for 2026 Projects
A concrete decision framework for 2026: start any new project by listing the utility functions you plan to use and checking MDN for native equivalents. Functions like groupBy, sortBy, flatten, uniq, and cloneDeep all have native equivalents in Node.js 18+ and modern browsers. For the remaining functions — primarily async utilities like mapLimit, retry, and parallel, plus typed object helpers like pick and omit with proper type narrowing — evaluate Radash. If Radash covers your remaining needs, you have a modern, maintained, zero-dependency utility layer with excellent TypeScript support. Add specific Lodash imports (lodash/debounce, lodash/merge, lodash/throttle) only for the functions Radash does not cover and that native JavaScript cannot replicate. This targeted approach typically reduces your utility library footprint by 80-90% compared to importing all of Lodash, while maintaining coverage for every actual use case.
Methodology
Download data from npm registry (weekly average, February 2026). Bundle size data from bundlephobia. Native JS compatibility from MDN and Node.js docs (v18 LTS baseline). Feature comparison based on Lodash v4.x, Radash v12.x, and ES2024 spec.
Compare utility and developer tool packages on PkgPulse →
See also: Lodash vs Underscore and Lodash vs Ramda, acorn vs @babel/parser vs espree.