Skip to main content

Guide

Lodash vs Radash vs Native JavaScript 2026

Compare Lodash, Radash, and native JavaScript for utility functions. Tree-shaking, TypeScript support, async utilities, bundle size, and when you no longer.

·PkgPulse Team·
0

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

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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.