Skip to main content

Lodash vs Radash vs Native JavaScript: Utility Functions in 2026

·PkgPulse Team

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.findLast cover 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, retry fill genuine gaps native JS doesn't cover

PackageWeekly DownloadsTypeScriptAsyncBundle Size
lodash~45M✅ @types~70KB full, per-fn minimal
radash~400K✅ Native~25KB
native JSN/A✅ Promises0KB

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

LodashNative AlternativeNode.js / ES Version
_.cloneDeepstructuredCloneNode 17+ / ES2022
_.groupByObject.groupByNode 21+ / ES2024
_.sortByArray.toSortedNode 20+ / ES2023
_.findLastArray.findLastNode 18+ / ES2023
_.flattenArray.flatNode 11+ / ES2019
_.flatMapArray.flatMapNode 11+ / ES2019
_.uniq[...new Set(arr)]Always
_.merge{ ...a, ...b }Always (shallow)
_.pick / _.omitDestructuringAlways
_.isEmptyobj && Object.keys(obj).length === 0Always
_.isNilvalue == nullAlways
_.isArrayArray.isArrayAlways
_.capitalizestr[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

FeatureLodashRadashNative JS
Weekly downloads~45M~400KN/A
TypeScript✅ @types/lodash✅ Native
Async utilities✅ mapLimit, retry, parallel✅ Promises
Bundle size~70KB (0 if tree-shaken)~25KB0KB
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
Dependencies000
Last major release2021 (v4)Active 2024Spec-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/omit with 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 _.merge for deep recursive object merging
  • You need debounce/throttle and don't want a separate package
  • You need _.template for 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.

Compare utility and developer tool packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.