fast-deep-equal vs dequal vs Lodash isEqual: Deep Equality in JavaScript (2026)
TL;DR
fast-deep-equal is the fastest pure deep equality library — ~50M weekly downloads, handles most types (objects, arrays, Date, RegExp, typed arrays) but not Maps, Sets, or circular references. dequal is slightly smaller and handles Maps, Sets, and TypedArrays correctly, making it more complete. Lodash _.isEqual is the most comprehensive — handles circular references, non-enumerable properties, WeakMaps, and edge cases that other libraries miss — at the cost of bringing in Lodash. For React render optimization: fast-deep-equal. For complete equality including Maps/Sets: dequal. For edge cases and circular refs: _.isEqual.
Key Takeaways
- fast-deep-equal: ~50M weekly downloads — fastest, ~400B, handles most types except Map/Set/circular
- dequal: ~10M weekly downloads — ~620B, handles Map/Set/TypedArray, no circular refs
- lodash.isequal: ~15M weekly downloads — most complete, handles circular refs, ~2KB standalone
JSON.stringify(a) === JSON.stringify(b)— don't do this (key order sensitivity, loses type info)Object.is(a, b)— only for primitives and reference equality, not deep- The performance difference only matters at scale — for most use cases, any of these is fine
Download Trends
| Package | Weekly Downloads | Bundle Size | Map/Set | Circular Refs | TypedArrays |
|---|---|---|---|---|---|
fast-deep-equal | ~50M | ~400B | ❌ | ❌ | ✅ |
dequal | ~10M | ~620B | ✅ | ❌ | ✅ |
lodash.isequal | ~15M | ~2KB | ✅ | ✅ | ✅ |
What Each Library Handles
// Objects and arrays — all three handle these:
isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }) // true ✅
// Dates:
isEqual(new Date("2026-01-01"), new Date("2026-01-01")) // true ✅ (all three)
// RegExp:
isEqual(/abc/gi, /abc/gi) // true ✅ (all three)
// Maps:
isEqual(new Map([["a", 1]]), new Map([["a", 1]]))
// fast-deep-equal: false ❌ (treats Map as object)
// dequal: true ✅
// _.isEqual: true ✅
// Sets:
isEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))
// fast-deep-equal: false ❌
// dequal: true ✅
// _.isEqual: true ✅
// Circular references:
const a: any = {}; a.self = a
const b: any = {}; b.self = b
isEqual(a, b)
// fast-deep-equal: stack overflow ❌
// dequal: stack overflow ❌
// _.isEqual: true ✅
// TypedArrays (Uint8Array, Float32Array, etc.):
isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])) // true ✅ (all three)
fast-deep-equal
fast-deep-equal — the performance choice:
Usage
import equal from "fast-deep-equal"
// Or:
import deepEqual from "fast-deep-equal/es6" // Includes ES6 type support
// Basic objects:
equal({ a: 1, b: 2 }, { a: 1, b: 2 }) // true
equal({ a: 1 }, { a: 1, b: 2 }) // false
// Arrays:
equal([1, 2, 3], [1, 2, 3]) // true
equal([1, [2, 3]], [1, [2, 3]]) // true
// Nested:
equal(
{ user: { name: "Alice", roles: ["admin", "user"] } },
{ user: { name: "Alice", roles: ["admin", "user"] } }
) // true
// Primitives:
equal("hello", "hello") // true
equal(1, 1) // true
equal(null, null) // true
equal(undefined, undefined) // true
React memo with fast-deep-equal
import { memo } from "react"
import equal from "fast-deep-equal"
// Default React.memo uses shallow equality — fast-deep-equal for deep:
const PackageCard = memo(
function PackageCard({ package: pkg }: { package: Package }) {
return (
<div>
<h3>{pkg.name}</h3>
<p>Score: {pkg.healthScore}</p>
<ul>{pkg.tags.map((tag) => <li key={tag}>{tag}</li>)}</ul>
</div>
)
},
equal // Custom comparator
)
// Now PackageCard only re-renders if the deep value actually changes
// (not just reference changes from parent re-renders)
Use in useEffect dependencies
import { useEffect, useRef } from "react"
import equal from "fast-deep-equal"
// Prevent effect from running when object value hasn't changed:
function useDeepEffect(effect: () => void | (() => void), deps: unknown[]) {
const prevDeps = useRef<unknown[]>([])
if (!equal(prevDeps.current, deps)) {
prevDeps.current = deps
}
useEffect(effect, [prevDeps.current])
}
// Usage:
function PackageAnalyzer({ filters }: { filters: PackageFilters }) {
useDeepEffect(() => {
fetchPackages(filters) // Only runs when filters deeply change
}, [filters])
}
dequal
dequal — complete equality with Map/Set support:
Basic usage
import { dequal } from "dequal"
// Everything fast-deep-equal handles:
dequal({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }) // true
dequal([1, 2, [3, 4]], [1, 2, [3, 4]]) // true
dequal(new Date("2026"), new Date("2026")) // true
// Plus Maps:
dequal(
new Map([["a", 1], ["b", 2]]),
new Map([["a", 1], ["b", 2]])
) // true
// Plus Sets:
dequal(new Set([1, 2, 3]), new Set([1, 2, 3])) // true
// Order matters for Sets? No:
dequal(new Set([1, 2, 3]), new Set([3, 2, 1])) // true (sets are unordered)
// But Map key order doesn't matter:
dequal(
new Map([["a", 1], ["b", 2]]),
new Map([["b", 2], ["a", 1]])
) // true
Lite version (no Map/Set, smaller)
import { dequal } from "dequal/lite" // ~350B, no Map/Set support
// Same API as dequal, but slightly smaller when you don't need Map/Set:
dequal({ a: 1 }, { a: 1 }) // true
dequal(new Map([["a", 1]]), new Map([["a", 1]])) // false (lite doesn't support Map)
Lodash _.isEqual
Lodash isEqual — the most complete equality checker:
Usage
import isEqual from "lodash/isEqual" // Tree-shakable import
// Or:
import { isEqual } from "lodash-es" // ESM version
// Everything the others handle, plus:
// Circular references:
const a: any = { name: "react" }
a.self = a // Circular!
const b: any = { name: "react" }
b.self = b // Circular!
isEqual(a, b) // true ✅ (others throw RangeError: Maximum call stack size exceeded)
// Error objects:
isEqual(new Error("oops"), new Error("oops")) // true ✅
// Arguments objects:
function test(...args: any[]) {
return isEqual(args, [1, 2, 3])
}
test(1, 2, 3) // true ✅
// Inherited properties:
class Base { value = 1 }
class Child extends Base { extra = 2 }
const obj1 = new Child()
const obj2 = new Child()
isEqual(obj1, obj2) // true (compares own + inherited enumerable properties)
When _.isEqual is the only option
import isEqual from "lodash/isEqual"
// Comparing Redux state with circular references from immer drafts:
function hasStateChanged(prev: AppState, next: AppState): boolean {
return !isEqual(prev, next)
}
// Comparing complex objects with potential circular structures:
const cache = new Map<string, ComplexObject>()
function hasChanged(key: string, newValue: ComplexObject): boolean {
const cached = cache.get(key)
if (!cached) return true
return !isEqual(cached, newValue) // Safe even if objects have circular refs
}
Performance Comparison
Benchmark: 1M iterations, medium-sized nested object
Object.is (reference): ~5ms (fastest — but only reference equality)
fast-deep-equal: ~180ms (fastest deep)
dequal: ~220ms
dequal/lite: ~200ms
_.isEqual (lodash): ~450ms
For most use cases, the difference is negligible.
Use fast-deep-equal when you're comparing thousands of times per second
(e.g., in a rendering loop or hot code path).
Feature Comparison
| Feature | fast-deep-equal | dequal | lodash.isEqual |
|---|---|---|---|
| Bundle size | ~400B | ~620B | ~2KB |
| Objects/arrays | ✅ | ✅ | ✅ |
| Date | ✅ | ✅ | ✅ |
| RegExp | ✅ | ✅ | ✅ |
| TypedArrays | ✅ es6 import | ✅ | ✅ |
| Map | ❌ | ✅ | ✅ |
| Set | ❌ | ✅ | ✅ |
| Circular refs | ❌ (crash) | ❌ (crash) | ✅ |
| WeakMap/WeakSet | ❌ | ❌ | ❌ |
| Performance | ⚡ Fastest | Fast | Moderate |
| TypeScript | ✅ | ✅ | ✅ |
When to Use Each
Choose fast-deep-equal if:
- Performance is critical — high-frequency comparisons in rendering loops
- You don't need Map/Set equality (most React state doesn't use them)
React.memocustom comparator — most component props are plain objects- You can guarantee no circular references in the data you're comparing
Choose dequal if:
- Your data structures include Maps, Sets, or TypedArrays
- You want a slightly more complete alternative to fast-deep-equal
- Still need small bundle size (620B vs 2KB)
- No circular references in your data
Choose lodash.isEqual if:
- Circular references are possible (deeply nested Redux state, graph structures)
- You're already using Lodash elsewhere (no additional cost)
- Edge cases matter more than performance
- Comparing complex class instances with inherited properties
Avoid JSON.stringify comparison:
// DON'T DO THIS:
JSON.stringify(obj1) === JSON.stringify(obj2)
// Problems:
// - Key order sensitivity: { a: 1, b: 2 } !== { b: 2, a: 1 }
// - Loses type info: Date becomes string
// - Throws on circular references
// - Much slower than fast-deep-equal
Methodology
Download data from npm registry (weekly average, February 2026). Performance benchmarks are approximate. Feature comparison based on fast-deep-equal v3.x, dequal v2.x, and lodash v4.x.