JavaScript utility libraries have been around since the language itself was painful to work with, and understanding where we came from makes it clearer why switching now makes sense.
A brief history: Underscore → Lodash → now
Underscore.js (2009) came first. Arrays didn’t have map or filter in IE, objects were hard to clone reliably, and Underscore gave you a consistent functional toolkit across browsers that disagreed on nearly everything.
Lodash (2012) forked from Underscore and quickly outgrew it, adding better performance, more functions, lazy evaluation chains, and deep object utilities that Underscore didn’t bother with. By the time ES6 arrived, Lodash had become the default dependency you added to every project without thinking.
The problem is that the JavaScript ecosystem it was built for no longer exists. Native arrays have map, filter, flatMap, findIndex. The spread operator handles shallow clones. Optional chaining removes the need for _.get. Modules are standard and TypeScript is everywhere.
Lodash kept adding functions and compatibility shims, and as the library grew, it became hard to tree-shake properly. Its types lagged behind, and it still ships a CommonJS-first build in a world moving toward ESM.
es-toolkit is the replacement, built after ES6, after TypeScript, after ESM, and it doesn’t carry any of Underscore’s legacy.
What’s actually different
Bundle size
Lodash’s full bundle is ~72kb minified. Even with tree-shaking, many bundlers struggle to eliminate unused code because of how Lodash was written: functions reference each other internally in ways that pull in more than you intended.
es-toolkit functions are isolated by design. Each one is a standalone ESM module with no internal cross-references, so tree-shaking works completely. A project that only uses chunk, groupBy, and debounce ships only those three.
TypeScript
Lodash’s types are maintained separately in @types/lodash. They lag behind, have inconsistencies, and often require manual type assertions to get right. Functions like _.groupBy return Dictionary<T[]> instead of Record<string, T[]>, and the inference doesn’t always propagate cleanly.
es-toolkit is written in TypeScript, so the types are part of the source rather than an afterthought. groupBy returns Record<string, T[]>, chunk returns T[][], and the types actually match what comes out.
Performance
es-toolkit benchmarks faster than Lodash across most operations. The library feels simpler because it doesn’t need to support IE or handle edge cases from a pre-ES6 world, and less code running means less time spent.
ESM-first
Lodash ships CJS. There are ESM builds (lodash-es) but they’re a separate package you have to explicitly install and import from. es-toolkit is ESM by default, with CJS as a secondary output, which matters for bundlers and for environments like Deno or edge runtimes that prefer or require ESM.
The functions you actually use
Most Lodash usage in practice comes down to a small set of functions. Here’s how they map:
| Lodash | es-toolkit |
|---|---|
_.chunk | chunk |
_.debounce | debounce |
_.throttle | throttle |
_.cloneDeep | cloneDeep |
_.groupBy | groupBy |
_.omit | omit |
_.pick | pick |
_.uniq | uniq |
_.uniqBy | uniqBy |
_.merge | merge |
_.flatten | flatten |
_.flattenDeep | flattenDeep |
_.isEqual | isEqual |
_.orderBy | orderBy |
_.intersection | intersection |
_.difference | difference |
The API surface is similar, and most migrations feel like a find-and-replace on import paths.
Switching
Install es-toolkit:
npm install es-toolkit
Uninstall Lodash:
npm uninstall lodash lodash-es @types/lodash @types/lodash-es
Then update your imports. If you were using Lodash like this:
import { groupBy, uniqBy, cloneDeep } from 'lodash'
the es-toolkit version is just:
import { groupBy, uniqBy, cloneDeep } from 'es-toolkit'
If you were using the lodash-es ESM package, the change is the same: just swap the import path.
For larger codebases, a codemod handles the mechanical part:
npx codemod lodash/to-es-toolkit
What es-toolkit doesn’t have
es-toolkit doesn’t implement every Lodash function. A few notable omissions:
_.chain: method chaining. This pattern fell out of style and es-toolkit doesn’t include it, so use native array methods or plain function composition instead._.template: string template compilation. Rarely used in modern code, and if you need it, keep it as a direct Lodash import._.invoke/_.invokeMap: uncommon in practice.
es-toolkit does include a compatibility layer for the most common Lodash functions if you need a bridge during migration:
import { groupBy } from 'es-toolkit/compat'
The compat build matches Lodash’s exact behavior, including edge cases and argument handling. Think of it as a stepping stone rather than a destination: migrate to the main package functions when you can.
What you should still use native for
Before reaching for es-toolkit, check if native JavaScript already covers it:
_.map(arr, fn)→arr.map(fn)_.filter(arr, fn)→arr.filter(fn)_.find(arr, fn)→arr.find(fn)_.reduce(arr, fn, init)→arr.reduce(fn, init)_.flatten(arr)→arr.flat()_.flatMap(arr, fn)→arr.flatMap(fn)_.get(obj, 'a.b.c')→obj?.a?.b?.c_.assign({}, a, b)→{ ...a, ...b }
If the native version is readable, use it. es-toolkit fills the gap for things that genuinely don’t have clean native equivalents: deep cloning, debounce, deep equality, grouping.
The bottom line
Lodash was the right tool for a decade, but the JavaScript it was designed to paper over is mostly gone now. es-toolkit is smaller, faster, TypeScript-native, and ESM-first. There’s no reason to start a new project with Lodash, and migrating an existing one is mostly a one-line change per import.