Performance Optimization and Benchmarking of the limu Immutable Data Library
This article details the design, three major benefits, optimization steps, and extensive benchmark results of the limu immutable data library, demonstrating how meta handling, prototype manipulation, and Map‑based storage dramatically improve performance compared to other libraries such as immer, mutative, and structura.
limu is designed for modern browsers, relying solely on Proxy environments and employing a "read‑time shallow copy, write‑time mark‑update" mechanism that lets users manipulate mutable data as if it were the original, generating proxy objects only for read nodes and producing a new structurally shared data snapshot after operations.
The design offers three key advantages:
Data structure transparency – limu is currently the only immutable library that allows real‑time inspection of draft data structures, making debugging friendly.
Excellent performance – by shallow‑copying only accessed paths and updating parent‑child links, the final draft creation checks a simple modified flag, achieving over 20× speedups against immer in read‑heavy scenarios.
Clean code structure – focusing exclusively on Proxy eliminates legacy baggage, allowing the library to concentrate on core immutable operations and facilitating future extensions.
Optimization Process
Before version 3.12, limu already outperformed immer by several times but still lagged behind newer libraries like structura and mutative . After 3.12, the gap was closed, and the following sections describe how limu reached peak performance.
Hiding Meta Data
Initially, meta information was stored directly on objects using a Symbol:
const LIMU_META = Symbol('LIMU_META');
const obj = { a: 1, b: { b1: 1 } };
// Reading obj.b creates a hidden meta on the object
{ b1: 1, [LIMU_META]: { parent, path, version, modified ... } }When a draft finished, delete was used to remove these meta properties, which left a visible LIMU_META key and polluted the output.
To avoid prototype pollution, the meta was later hidden in __proto__ by inserting a clean prototype object between the original prototype chain:
before:
a.__proto__ -> Object.prototype
after:
a.__proto__ -> Limu.prototype -> Object.prototype
// Each Limu.prototype is created via Object.create(null)The injection function looks like:
export function injectLimuMetaProto(rawObj) {
const pureObj = Object.create(null);
Object.setPrototypeOf(pureObj, Object.prototype);
Object.setPrototypeOf(rawObj, pureObj);
return rawObj;
}This approach combines Symbol‑based private fields with a rebuilt prototype chain, keeping meta data hidden without contaminating the original prototype.
Performance Loss Analysis
Even with meta handling, limu still lagged 4‑5× behind structura and mutative . Profiling identified two major cost centers: the delete operation used to clean meta and the repeated Object.setPrototypeOf calls.
Removing delete from the draft finalization doubled performance, while replacing prototype manipulation with direct property assignment ( obj[LIMU_META] = newMeta() ) yielded a 4× speedup.
Custom Optimization Strategy
To eliminate both costly delete and setPrototypeOf while preserving clean data and accessible meta, a Map was introduced to store meta information per draft:
function getProxy(metaMap, dataNode) {
const meta = !getMeta(dataNode);
if (!meta) {
const copy = shallowCopy(dataNode);
const meta = newMeta(copy, dataNode);
// Use the shallow copy as the key in the map
metaMap[copy] = meta;
}
// Return the previously generated proxy
return meta.proxyVal;
}
function createDraft(base) {
const metaMap = new Map();
return new Proxy(base, {
get(parent, key) {
const dataNode = parent[key];
return getProxy(dataNode);
},
});
}During finishDraft , meta entries are simply removed from the map via map.delete , which is far more efficient than delete on objects.
Benchmark Setup
Benchmarks were run on a 2021 MacBook Pro (M1, 32 GB RAM) using six libraries: immer , limu , mutative , structura , pstr (JSON clone), and native . Four scenarios were tested:
Non‑frozen, no array operations (s1)
Non‑frozen, array operations (s2)
Frozen, no array operations (s3)
Frozen, array operations (s4)
Each scenario executed 1 000 loops of a complex read/write workload on a 20‑key object with 26‑level depth and a 10 000‑element array.
Benchmark Results
In non‑frozen cases, limu , structura , and mutative performed within a 10‑30× range of each other, far faster than immer and pstr . In frozen scenarios, limu consistently led, with mutative second, while structura and immer lagged behind even the hard‑clone pstr approach.
❝ Note: Smaller area indicates better performance ❞
Conclusion
Through systematic meta handling, prototype restructuring, and Map‑based storage, limu now matches or exceeds the performance of competing immutable libraries across both frozen and non‑frozen workloads, while keeping data transparent and debugging‑friendly. Its API remains compatible with immer , making it a drop‑in replacement for projects that target modern Proxy ‑enabled browsers.
One More Thing
Based on limu, the upcoming helux framework aims to redefine React development by improving both developer experience (DX) and user experience (UX). Stay tuned for the announcement.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.