Skip to main content

fast-deep-equal vs dequal vs Lodash isEqual: Deep Equality in JavaScript (2026)

·PkgPulse Team

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

PackageWeekly DownloadsBundle SizeMap/SetCircular RefsTypedArrays
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

Featurefast-deep-equaldequallodash.isEqual
Bundle size~400B~620B~2KB
Objects/arrays
Date
RegExp
TypedArrays✅ es6 import
Map
Set
Circular refs❌ (crash)❌ (crash)
WeakMap/WeakSet
Performance⚡ FastestFastModerate
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.memo custom 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.

Compare utility and JavaScript packages on PkgPulse →

Comments

Stay Updated

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