Skip to main content

klona vs rfdc vs structuredClone: Deep Copy in JavaScript (2026)

·PkgPulse Team

TL;DR

structuredClone is the modern built-in — available in Node.js 17+, all major browsers, Deno, and Bun. It handles Map, Set, Date, ArrayBuffer, circular references, and many other types correctly, with zero dependencies. rfdc (Really Fast Deep Clone) is the fastest library option — 3-5x faster than JSON round-trip, handles dates, and has minimal footprint. klona is the smallest library — 700B, handles all common JavaScript types, ESM-native. In 2026, reach for structuredClone first. Only add a library if you need performance-critical cloning in hot paths, or if you need Node.js 16 support.

Key Takeaways

  • structuredClone: Built-in (0KB) — Node.js 17+, handles Map/Set/Date/circular refs, W3C standard
  • rfdc: ~10M weekly downloads — fastest library, 3-5x faster than JSON, ~500B
  • klona: ~5M weekly downloads — 700B, excellent type coverage, ESM-native
  • JSON.parse(JSON.stringify(obj)) drops undefined, corrupts Date, ignores Symbol — avoid for deep copy
  • structuredClone does NOT clone functions, class instances lose their prototype
  • Use rfdc or klona only in performance-critical hot paths — structuredClone is fine for most cases

The Deep Copy Problem

// Shallow copy — WRONG for nested objects:
const original = { user: { name: "Royce", settings: { theme: "dark" } } }
const shallow = { ...original }

shallow.user.settings.theme = "light"
console.log(original.user.settings.theme)  // "light" — MUTATED!

// JSON round-trip — common but broken:
const jsonCopy = JSON.parse(JSON.stringify(original))

// JSON round-trip limitations:
JSON.parse(JSON.stringify({
  date: new Date(),           // → string (Date lost)
  undef: undefined,           // → dropped entirely
  fn: () => {},              // → dropped entirely
  map: new Map([["a", 1]]),  // → {} (empty object)
  set: new Set([1, 2, 3]),   // → {} (empty object)
  regex: /pattern/g,         // → {} (empty object)
  num: NaN,                  // → null
  inf: Infinity,             // → null
}))
// Only works correctly for plain JSON-compatible data

structuredClone (built-in)

Built into JavaScript runtimes — no install needed:

Basic usage

// Available in Node.js 17+, all modern browsers, Deno, Bun:

const original = {
  name: "pkgpulse",
  config: {
    features: ["ssr", "analytics"],
    settings: { theme: "dark", locale: "en" },
  },
  createdAt: new Date("2024-01-01"),
  tags: new Set(["typescript", "nextjs"]),
  meta: new Map([["version", "1.0.0"], ["author", "Royce"]]),
}

const clone = structuredClone(original)

// Mutations don't affect original:
clone.config.settings.theme = "light"
clone.config.features.push("ai")

console.log(original.config.settings.theme)    // "dark" — untouched
console.log(original.config.features.length)   // 2 — untouched

// Types are preserved:
console.log(clone.createdAt instanceof Date)   // true (not a string)
console.log(clone.tags instanceof Set)          // true
console.log(clone.meta instanceof Map)          // true

Circular reference support

// structuredClone handles circular references — JSON.parse/stringify throws:
const obj: Record<string, unknown> = { name: "circular" }
obj.self = obj  // Circular reference

// JSON.parse(JSON.stringify(obj))  // TypeError: Converting circular structure to JSON

const clone = structuredClone(obj)
// Works! clone.self === clone (internal reference preserved)
console.log(clone.self === clone)  // true

What structuredClone does NOT support

// Functions are NOT cloned — throws DataCloneError:
// structuredClone({ fn: () => {} })
// → DataCloneError: () => {} could not be cloned

// Class instances lose their prototype:
class Packet {
  constructor(public data: string) {}
  serialize() { return this.data }
}

const packet = new Packet("hello")
const clone = structuredClone(packet)
console.log(clone instanceof Packet)  // false — it's a plain object
// clone.serialize  // undefined — methods are gone!

// DOM nodes, WeakMap, WeakSet, Proxy — not supported
// Error objects lose their class too

// Workaround for classes — implement custom clone:
class Packet {
  constructor(public data: string) {}
  clone() { return new Packet(this.data) }
}

Transfer (move, not copy)

// structuredClone can TRANSFER ownership (zero-copy for ArrayBuffer):
const buffer = new ArrayBuffer(1024)
const view = new Uint8Array(buffer)
view[0] = 255

const clone = structuredClone(buffer, { transfer: [buffer] })
// buffer is now detached — zero-copy transfer
console.log(buffer.byteLength)  // 0 — detached!
console.log(clone.byteLength)   // 1024 — new owner

// Useful for Worker thread message passing without copying large buffers

rfdc

rfdc — Really Fast Deep Clone:

Basic usage

import rfdc from "rfdc"

// Create a clone function (configure once):
const clone = rfdc()

const original = {
  name: "react",
  nested: { deep: { value: 42 } },
  date: new Date("2024-01-01"),
  arr: [1, 2, { three: 3 }],
}

const copy = clone(original)
copy.nested.deep.value = 99

console.log(original.nested.deep.value)  // 42 — untouched
console.log(copy.date instanceof Date)   // true — Date preserved

Configuration options

import rfdc from "rfdc"

// Default — fastest, no circular reference support:
const clone = rfdc()

// With circular reference support (slightly slower):
const cloneCircular = rfdc({ circles: true })

const obj: any = { a: 1 }
obj.self = obj

const copy = cloneCircular(obj)
// Works! copy.self === copy

// Proto — copy prototype (default: false):
const cloneWithProto = rfdc({ proto: true })

class Config {
  value = 42
  get double() { return this.value * 2 }
}

const config = new Config()
const copied = cloneWithProto(config)
// copied.double still works — prototype methods copied

Performance comparison

// rfdc is consistently the fastest deep clone library:
// (100K iterations of a moderately nested object)

// JSON round-trip:          ~500ms (baseline)
// lodash cloneDeep:         ~400ms (1.25x faster than JSON)
// structuredClone:          ~350ms (1.4x faster than JSON)
// klona:                    ~200ms (2.5x faster than JSON)
// rfdc:                     ~150ms (3.3x faster than JSON)
// rfdc (with proto:true):   ~180ms (2.8x faster than JSON)

// When to care about this: streaming pipelines, game loops,
// high-frequency event processing — not typical CRUD operations

Hot path usage

import rfdc from "rfdc"

const clone = rfdc()

// Example: package data pipeline processing 10K packages/sec
function processPackageStream(packages: Package[]) {
  return packages
    .map(clone)              // Clone before mutation (rfdc is fast here)
    .map(normalizePackage)   // Mutate the clone safely
    .filter(isHealthy)
}

// In React — cloning state for immutable updates:
function reducer(state: AppState, action: Action): AppState {
  const next = clone(state)
  // ... apply action mutations to next
  return next
}

klona

klona — 700B deep clone library:

Basic usage

import { klona } from "klona"
// Or: import klona from "klona/full"  // Includes TypedArrays, DataView, etc.

const original = {
  name: "typescript",
  config: {
    strict: true,
    paths: { "@/*": ["src/*"] },
  },
  date: new Date(),
  arr: [1, [2, [3]]],
  map: new Map([["key", "value"]]),
  set: new Set([1, 2, 3]),
}

const copy = klona(original)

copy.config.strict = false
copy.date.setFullYear(2020)
copy.arr[1][0] = 99

console.log(original.config.strict)  // true — untouched
console.log(original.date.getFullYear())  // current year — untouched
console.log(original.arr[1][0])      // 2 — untouched

klona variants

// klona ships multiple variants for different use cases:

// klona/json — fastest, JSON types only (no Date/Map/Set):
import { klona } from "klona/json"

// klona/lite — handles Date + RegExp but not Map/Set/TypedArray:
import { klona } from "klona/lite"

// klona (default) — handles Date, Map, Set, RegExp, DataView:
import { klona } from "klona"

// klona/full — everything including TypedArray, ArrayBuffer, DataView, Symbol:
import { klona } from "klona/full"

ESM and tree-shaking

// klona is ESM-native — works well with bundler tree-shaking:
// Only import what you use:

import { klona } from "klona"    // ~700B
import { klona } from "klona/json"  // ~200B (just JSON types)

// Compare to lodash cloneDeep:
// import { cloneDeep } from "lodash"  // ~15KB (pulls in lodash internals)
// import cloneDeep from "lodash/cloneDeep"  // ~5KB (better but still heavy)

Feature Comparison

FeaturestructuredClonerfdcklona
Install required❌ Built-in
Bundle size0KB~500B~700B
Map/Set
Date
RegExp
TypedArray✅ (full)
Circular refs✅ (opt-in)
Functions❌ (throws)
Class prototype✅ (opt-in)
PerformanceFast⚡ FastestVery fast
Node.js 16
ESMN/A

When to Use Each

Use structuredClone (built-in) if:

  • Node.js 17+ / modern browser (2022+) — this covers most projects in 2026
  • You need Map, Set, Date, circular reference support
  • Zero dependencies is a priority
  • General-purpose deep cloning of state, config, data

Use rfdc if:

  • Performance-critical hot paths (game loops, streaming data pipelines)
  • Need the fastest possible clone with minimal footprint
  • Node.js 16 support needed
  • Payload is plain objects + arrays + Dates (no Map/Set needed)

Use klona if:

  • Node.js 16 support needed but you want Map/Set/RegExp support
  • Prefer a library with multiple precision variants (klona/json vs klona/full)
  • ESM-first codebase with tree-shaking

Avoid these patterns:

// JSON round-trip — loses types, don't use:
const bad = JSON.parse(JSON.stringify(obj))

// lodash cloneDeep — 5KB+ for a single utility:
import cloneDeep from "lodash/cloneDeep"  // Use structuredClone instead

// Object.assign — shallow only:
const shallow = Object.assign({}, nested)  // nested props still shared

// Spread — also shallow:
const stillShallow = { ...nested }  // Same problem

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on rfdc v1.x, klona v2.x, and the W3C Structured Clone Algorithm specification (Node.js 17.0+ implementation).

Compare utility and data processing packages on PkgPulse →

Comments

Stay Updated

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