Lodash vs Radash vs Native JavaScript: Utility Functions in 2026
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
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.