perf(core): add container-tree initialization and lazy list views - #98
Merged
Merged
Conversation
…call
Mirror initialization previously walked the document container by
container (~10 wasm crossings per container: id/kind/get/keys/toJSON).
buildRootStateSnapshot now reads the whole doc with a single
doc.getDeepValueWithID() call and builds state, $cid markers,
containerRegistry, inferOptionsByContainerId and rootPathById in one JS
walk that replicates registerChildContainer/containerToMirrorState
semantics exactly. No container handles are created during init; trees
and unhandled kinds fall back to the existing handle-based logic, and
the legacy traversal is kept as a fallback for loro-crdt versions
without getDeepValueWithID.
- Defensive cid parsing: tolerates both the Debug-like
"idx:N, id:cid:..." format and bare container ids.
- loro-crdt <1.15 drops the {value,cid} wrapper for empty containers,
making them indistinguishable from embedded plain values; the walk
disambiguates via the parent's cached getShallowValue() (one extra
call per affected parent only).
- Adds a seeded parity fuzz test (bulk vs legacy: state, $cid via
cidsEqual, registry, infer options) and a dependency-free benchmark
(pnpm bench:bulk-init, ~67k-container session-history-shaped doc):
wasm crossings during init drop from ~527k to 1.
This was referenced Sep 6, 2026
zxch3n
changed the base branch from
perf/bulk-init-deep-value
to
fix/ignore-root-events
September 6, 2026 15:46
* perf(core): copy paths for single text events Avoid general Immer drafting for existing text leaves while retaining descriptors and immutable snapshots. Add real-replica parity checks, a deterministic fast-path guard, and a reproducible event-application benchmark. Model: gpt-5 * perf(core): copy dense array ancestors without defining each property Validate ordinary array descriptors before direct value copying and retain descriptor fallback for unusual arrays. Move differential assertions outside Wasm callbacks, add array parity and work guards, expand paired benchmarks, and move implementation notes below the quick start. Model: gpt-5 * fix(core): route array subclasses through general event handling Check array prototypes during path traversal, remove the redundant dense-copy guard, and verify subclass prototype parity with the general path. Removing the guard now fails the subclass regression. Model: gpt-5
* feat(core): add lazy list option and LazyList read view
schema.LoroList(item, idSelector, { lazy: { index, maxHydrated?, tailKeep? } })
keeps large lists out of memory: Mirror state holds a stable LazyList
instance at the lazy key instead of an array. Ids come from one
list.getShallowValue() call; the fields named in lazy.index are always
loaded (extracted from the bulk deep-value payload when available, one
map.getShallowValue() per item otherwise); items hydrate on demand via
hydrate(from, to) with LRU eviction (maxHydrated, default 200) that
exempts active subscribeRange windows and the trailing tailKeep items
(default 20). Hydrated data is keyed by item container id, never by
position, so positional views stay correct across concurrent edits.
- Loro events are intercepted before the immutable state walk:
structural deltas update ids/length/version and shift hydrated slots
by id; events inside hydrated items reuse applyEventBatchToState
(structural sharing identical to non-lazy lists); events inside
non-hydrated items only refresh index fields and bump version.
- setState updates touching a lazy path throw LazyListWriteError and
point to mirror.list(path); the write API
(push/insert/deleteById/updateById/updateAt) reuses the existing
diff/apply machinery so it produces exactly the Loro ops an
equivalent setState array edit would (round-trip tested).
- Lazy lists are excluded from the ephemeral compose;
checkStateConsistency verifies lazy paths by instance identity.
- InferType/InferInputType resolve lazy lists to
LazyList<Item, Partial<Item>>; LazyList, LazyListImpl,
LazyListWriteError, isLazyList and LazyListWriter are exported.
- React: useLazyRange(list, from, to) hydrates a window, re-renders on
in-range changes via subscribeRange, and releases on unmount.
- Benchmark (scripts/lazy-list-bench.mjs, 2000 items x ~30 nested
containers, ~58k containers): lazy init p50 20.5 ms, hydrate(0, 30)
p50 2.1 ms, eager control (bulk init) p50 117.7 ms.
* fix(core): preserve lazy item batches and hydration lifetimes
Model: gpt-6
* chore: validate lazy lists on loro-crdt 1.16.0
* perf(core): hydrate lazy items with container trees
* fix: preserve nested lazy updates and observe tail growth
* fix(core): normalize inserted lazy items and release deleted subtrees
Keep bulk, lazy-list and single-text-event benchmark commands. Model: gpt-6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mirror initializes from selected
LoroDoc.toContainerTree()roots and exposes opt-in LazyList read views for schema-declared lists. Explicit container types/IDs distinguish containers from opaqueValuenodes, avoiding per-parent shallow rereads. Lazy lists retain indexes and hydrate requested item subtrees; writes usemirror.list(path).This PR includes the bulk foundation formerly reviewed as #95 and the merged #96 LazyList implementation. Its base is #97 (
fix/ignore-root-events). Review the complete current diff, including LazyList; #96 is no longer a separate pending stack entry.The actual dependency is loro-crdt 1.16.0. Older API compatibility paths remain; the documented container-tree nesting-limit error falls back to handle reads. Existing nested insertion/deletion lifecycle and literal-container-ID regressions are covered.
Main has been merged, preserving both #99's single-text-event optimization/benchmark and the bulk/lazy benchmarks. Validation after integration:
pnpm checkpasses (build, 618 tests / 59 files, lint 19 warnings / 0 errors, typecheck).A separate follow-up fixes nested scalar Ignore leakage, lazy setState guards below eager lists, and lazy Tree data initialization. Those repairs are not included in the validation above. These are library-level checks, not a 3000-round Lody end-to-end acceptance result.
Follow-up repairs: #101.