Frontmatter
| id | 8867 |
| title | feat: Implement incremental updates for Card Layouts |
| state | Open |
| labels | enhancementaiperformance |
| assignees | [] |
| createdAt | Jan 23, 2026, 7:13 PM |
| updatedAt | Jan 23, 2026, 8:24 PM |
| githubUrl | https://github.com/neomjs/neo/issues/8867 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
feat: Implement incremental updates for Card Layouts

Input from Gemini 3 Pro:
✦ ### Feasibility Analysis & Retrospective (2026-01-23)
We attempted to implement "Incremental Updates" for
Cardlayout to optimize performance by reducing VDOM serialization and IPC payload. The goal was to switchCardlayout updates fromupdateDepth: -1(Full Tree) toupdateDepth: 1(Container Shell), while explicitly merging the New Active Item (Full) and Old Active Item (Shell).The Approach
- Strategy: Use
VDomUpdate.registerMergedto whitelist the active/inactive cards for expansion, while lettingTreeBuilderprune all other items atdepth: 1.- Mechanism:
- New Active Item: Registered with
depth: -1(Full Expansion).- Old Active Item: Registered with
depth: 1(Shell only, to toggleremoveDomor CSS classes).- Others: Pruned by
TreeBuilder(sent as{componentId: ...}placeholders).The Roadblocks
1. The Pruning vs. Preservation Conflict When
TreeBuilderprunes a component (because it's not active/merged), it sends a Placeholder ({componentId: ...}).
- If the component was previously fully rendered:
Helper(VDom Worker) seesNew=PlaceholdervsOld=Element.- Standard Behavior:
Helpertreats this as a node replacement. It removes the Element (destroying DOM content) and inserts the Placeholder. This wipes out the UI for inactive tabs that shouldn't be destroyed (ifremoveInactiveCards: false).- Attempted Fix: We tried to use
neoIgnore: trueto tellHelperto "keep the existing DOM".2. State Desynchronization & Duplication Using
neoIgnoreon a Placeholder creates a dangerous state desynchronization:
- The Lie:
Helperreceives a Placeholder and "accepts" it as the new VNode state, but skips DOM updates (leaving the full DOM tree intact).- The Consequence:
Helper's internal state (and thevnodesent back to App Worker) now thinks the component is empty (Placeholder).- The Crash: On the next update (when the card becomes active again), we send the Full Tree.
HelpercomparesNew=ElementvsOld=Placeholder.
Helpersees thatOldhas 0 children.Newhas N children.HelpergeneratesinsertNodefor N children.- Result: These nodes are appended to the existing DOM nodes (which were never removed), causing Content Duplication (double rendering).
3. The VNode Reference Integrity The App Worker relies on
component._vnodeto track mounted state.
- When
Helperreturns the Placeholder (from the "pruned" update),syncVnodeTreeupdatescomponent._vnodeto the Placeholder.- This wipes out the App Worker's knowledge of the component's internal structure.
- Subsequent logic relying on
vnodetraversal (e.g.,onScrollCapturelooking up IDs, orTreeBuilderrecursion) fails or errors (Cannot read properties of null).4. Recursive Merging Gaps We discovered that
VDomUpdate.getMergedChildIdsdid not natively support recursive merging (Grandchild -> Child -> Parent).
- If a Grandchild updated (merged to Child) and the Child updated (merged to Parent), the Parent's
TreeBuilderonly saw the Child as "dirty", not the Grandchild.- Result: The Grandchild was aggressively pruned, losing its update.
Conclusion
The "Incremental Update" strategy requires a fundamental architectural change in how
HelperandTreeBuilderhandle pruning. To make this work, Pruning must be distinguishable from removal.
- Current: Pruning = Placeholder. Placeholder = "Empty Node".
- Required: Pruning = "Keep Existing State".
- This requires
Helperto copy the Old VNode (Full Tree) into the New VNode structure when ignoring/pruning, so that the state remains "Full".- We attempted this ("State Swap" fix), but it introduced significant complexity and regression risks in
RealWorldUpdatestests, indicating edge cases in diffing mixed trees (Placeholder vs Element).For now, the complexity and risk of destabilizing the core VDOM engine outweigh the performance benefits. Future attempts should focus on enabling
Helperto safely "carry forward" full trees when receiving placeholders, effectively allowing "Sparse VDOM Updates" without state corruption.
Description: Optimize
Neo.layout.Cardperformance by implementing an "Incremental Update" strategy. This reduces the serialization and IPC overhead when switching between cards, especially in containers with many items or complex structures.Current Behavior: Switching a card triggers a full container update (
updateDepth: -1). This causes the App Worker to build and serialize the entire VDOM tree (including inactive items) and sends it to the VDom Worker. WhileremoveInactiveCards: truekeeps the live DOM pruning efficient, the VDOM generation and message passing remain O(N) where N is the total complexity of all cards.Proposed Optimization: Introduce a new config
incrementalUpdates(Boolean, defaulttrue) toNeo.layout.Card.When
true:depth: -1) viaVDomUpdate.registerMerged(). This ensures its entire subtree is serialized and rendered. This is CRITICAL as the item may be transitioning from a pruned state.depth: 1) viaVDomUpdate.registerMerged(). This allows applying theinactiveCSS class orremoveDom: trueflag while pruning its children (sendingneoIgnorestubs).TreeBuilderat depth 1, effectively reducing the payload to O(1) (Active Card + Shells).Constraints & Requirements:
true.neoIgnorestubs. It is incompatible with DOM move operations involving these stubs. Layouts that perform DOM reordering (likeCubelayout) or drag-and-drop scenarios must either disable this config or ensure the VDOM worker handles stub moves gracefully (currently risky).slideDirection.Acceptance Criteria:
incrementalUpdatesconfig added toNeo.layout.Card(default:true).afterSetActiveIndex.