immutable, lodash, ramda, and underscore are widely used JavaScript utility libraries that help developers work with data more effectively. immutable provides persistent, immutable data structures like Map, List, and Set that enable efficient updates without mutating original values. lodash offers a comprehensive suite of utility functions for common programming tasks, including array manipulation, object operations, and functional helpers, with a focus on performance and broad compatibility. ramda emphasizes a functional programming style with automatic currying, immutable operations, and a data-last argument order that supports point-free composition. underscore is an early utility library that provides foundational functional helpers and was influential in shaping JavaScript’s utility landscape, though it has seen reduced adoption in modern projects.
When working with data in JavaScript—especially in complex frontend applications—you often need more than the built-in Array and Object methods. The libraries immutable, lodash, ramda, and underscore each offer different approaches to handling data transformations, immutability, and functional composition. Let’s compare them through real-world engineering lenses.
immutable enforces true immutability using persistent data structures. Every update returns a new instance, but shares unchanged parts under the hood for efficiency.
import { Map } from 'immutable';
const original = Map({ name: 'Alice', age: 30 });
const updated = original.set('age', 31);
console.log(original.get('age')); // 30
console.log(updated.get('age')); // 31
console.log(original === updated); // false — but memory-efficient
lodash provides mutable and immutable helpers, but defaults to returning new values (e.g., _.cloneDeep, _.map). It doesn’t enforce immutability but makes it easy to achieve.
import _ from 'lodash';
const original = { name: 'Alice', age: 30 };
const updated = _.assign({}, original, { age: 31 });
// Or use _.set with a clone
const updated2 = _.set(_.cloneDeep(original), 'age', 31);
ramda assumes immutability by default and never mutates inputs. All functions return new values, and it encourages pure, side-effect-free transformations.
import * as R from 'ramda';
const original = { name: 'Alice', age: 30 };
const updated = R.assoc('age', 31, original);
// Original unchanged; updated is a new object
underscore behaves similarly to early lodash—it offers utility functions that typically return new values but doesn’t enforce immutability or provide deep immutable structures.
import _ from 'underscore';
const original = { name: 'Alice', age: 30 };
const updated = _.extend({}, original, { age: 31 });
⚠️ Note: As of 2024,
underscoreis in maintenance mode. The official npm page states it’s “no longer under active development.” Avoid it in new projects.
lodash supports method chaining via _.chain() or the _(value) wrapper, which is intuitive for imperative-style pipelines.
import _ from 'lodash';
const result = _(users)
.filter(u => u.active)
.map(u => u.name)
.take(5)
.value();
ramda favors function composition over chaining. Its data-last signature enables natural point-free style with R.compose or R.pipe.
import * as R from 'ramda';
const getActiveNames = R.pipe(
R.filter(R.prop('active')),
R.map(R.prop('name')),
R.take(5)
);
const result = getActiveNames(users);
immutable uses method chaining on its own data types, which is fluent but locks you into its ecosystem.
import { List } from 'immutable';
const usersList = List(users);
const result = usersList
.filter(u => u.active)
.map(u => u.name)
.take(5);
underscore supports chaining too, but without tree-shaking or modern optimizations.
import _ from 'underscore';
const result = _.chain(users)
.filter(u => u.active)
.map(u => u.name)
.first(5)
.value();
immutable requires conversion to/from plain JS objects using .toJS() and fromJS(), which can hurt performance if overused.
import { fromJS } from 'immutable';
const jsObj = { a: 1 };
const imMap = fromJS(jsObj);
const backToJs = imMap.toJS(); // expensive for large structures
lodash, ramda, and underscore all work directly with standard JavaScript types—no conversion needed.
// All of these work natively with { a: 1 }
_.get(obj, 'a');
R.prop('a', obj);
_.property('a')(obj); // underscore
This makes lodash and ramda easier to integrate incrementally into existing codebases.
ramda automatically curries all functions, enabling partial application without extra syntax.
import * as R from 'ramda';
const pickName = R.pick(['name']);
const names = R.map(pickName, users); // or compose further
lodash provides explicit currying via _.curry() but doesn’t curry by default.
import _ from 'lodash';
const add = _.curry((a, b) => a + b);
const add5 = add(5);
immutable and underscore do not support currying.
Use immutable if:
Avoid if you need seamless JSON interoperability or are sensitive to bundle size from non-tree-shakable APIs.
Use lodash if:
import map from 'lodash/map') for tree-shaking.It’s the safest choice for mixed-paradigm codebases.
Use ramda if:
Be cautious if your team isn’t familiar with FP—it can raise the cognitive barrier.
underscore should not be used in new projects. Its functionality is largely superseded by lodash, which began as a fork of underscore and has since evolved with better performance, modularity, and modern JavaScript support.
| Feature | immutable | lodash | ramda | underscore |
|---|---|---|---|---|
| Immutability | ✅ Enforced (custom types) | ✅ Optional (plain JS) | ✅ Default (plain JS) | ⚠️ Not enforced |
| Currying | ❌ | ✅ Manual | ✅ Automatic | ❌ |
| Chaining | ✅ (on Immutable types) | ✅ | ✅ (via pipe/compose) | ✅ |
| Plain JS Interop | ❌ (requires conversion) | ✅ | ✅ | ✅ |
| Modern Ecosystem Fit | ⚠️ Niche (Redux era) | ✅ Excellent | ✅ Strong (FP teams) | ❌ Deprecated/maintenance |
| Tree-shaking Support | ⚠️ Limited | ✅ Yes | ✅ Yes | ❌ No |
lodash for general-purpose utilities. It’s pragmatic, fast, and widely understood.ramda to unlock expressive, composable data flows.immutable only if you’ve measured performance gains and accept the ecosystem cost.underscore in new work—it’s a relic of JavaScript’s past.All four libraries solve real problems, but only three remain relevant today. Choose based on your team’s style, performance requirements, and long-term maintainability—not just feature checklists.
Choose lodash when you need a battle-tested, highly optimized utility belt for everyday tasks across arrays, objects, strings, and functions. Its modular design allows tree-shaking, and its consistent API works well in both browser and Node.js environments. It’s especially valuable in large codebases requiring robust, readable utilities without enforcing a specific programming paradigm.
Choose immutable when you need guaranteed immutability with persistent data structures that support structural sharing for performance. It's ideal for applications using Redux or other state management systems where deep immutability and change detection are critical. However, be aware that it introduces a learning curve due to its custom types and requires interop utilities when interfacing with plain JavaScript objects.
Choose underscore only for maintaining legacy codebases that already depend on it. While it pioneered many utility patterns in JavaScript, it lacks modern features like tree-shaking support and hasn't kept pace with current ecosystem expectations. For new projects, prefer lodash or ramda depending on your architectural needs.
Choose ramda when you're building a codebase that embraces functional programming principles like pure functions, immutability, and composition. Its automatic currying and data-last parameter order make it easy to create reusable, composable pipelines. It’s best suited for teams comfortable with FP concepts and looking to reduce boilerplate in data transformation logic.
The Lodash library exported as Node.js modules.
Using npm:
$ npm i -g npm
$ npm i --save lodash
In Node.js:
// Load the full build.
var _ = require('lodash');
// Load the core build.
var _ = require('lodash/core');
// Load the FP build for immutable auto-curried iteratee-first data-last methods.
var fp = require('lodash/fp');
// Load method categories.
var array = require('lodash/array');
var object = require('lodash/fp/object');
// Cherry-pick methods for smaller browserify/rollup/webpack bundles.
var at = require('lodash/at');
var curryN = require('lodash/fp/curryN');
See the package source for more details.
Note:
Install n_ for Lodash use in the Node.js < 6 REPL.
Tested in Chrome 74-75, Firefox 66-67, IE 11, Edge 18, Safari 11-12, & Node.js 8-12.
Automated browser & CI test runs are available.