Optimization Principles of List Rendering in Virtual DOM
Description
List rendering is a high-frequency operation scenario in frontend frameworks. When list items change, how to efficiently update the DOM is crucial for framework performance. The Virtual DOM compares old and new virtual node trees through a diff algorithm, but a simple, brute-force recursive comparison performs poorly in list scenarios (O(n³)). Modern frameworks employ various optimization strategies to reduce complexity to O(n), with the core idea being to reuse DOM nodes as much as possible.
Problem-Solving Process
1. Root Cause: Performance Bottleneck of Traditional Diff Algorithms
- When comparing two arrays of old child nodes and new child nodes, the most straightforward algorithm is a double loop traversal.
- For each new node, traverse all old nodes to find a reusable node.
- Time complexity is O(n²). Combined with recursion over the tree structure, the overall complexity can reach O(n³).
- In scenarios with large-scale data updates, this algorithm leads to significant performance issues.
2. Basic Optimization: Adding Key Identifiers
- Frameworks require developers to provide a unique
keyattribute for list items. - The purpose of
key: Provides a stable identifier for virtual nodes, avoiding incorrect reuse based on index. - Implementation principle:
- Create a mapping table from the old nodes' keys to their indices:
{key1: 0, key2: 1, key3: 2} - Traverse the new nodes, quickly find reusable old nodes via the key (O(1) time complexity).
- Without a
key, comparison can only be based on index position, leading to incorrect updates when the list order changes.
- Create a mapping table from the old nodes' keys to their indices:
3. Double-Ended Comparison Algorithm (Head-Tail Pointer Optimization)
- Simultaneously compare from the heads and tails of the old and new lists towards the middle, handling common reordering scenarios.
- Implementation steps:
- Head-to-head comparison: Compare nodes pointed to by the head pointers of the old and new lists.
- Tail-to-tail comparison: Compare nodes pointed to by the tail pointers of the old and new lists.
- Cross comparison: Compare the new head with the old tail, and the new tail with the old head (handles rotation scenarios).
- Remaining processing: Handle the disordered middle section via the key mapping.
- Vue 2.x uses this algorithm, performing well in most practical scenarios.
4. Longest Increasing Subsequence Algorithm (Vue 3 Optimization)
- Vue 3 builds upon the double-ended comparison and further optimizes by introducing the Longest Increasing Subsequence (LIS).
- Core idea: Find the continuous sequence of nodes whose relative positions remain unchanged between the old and new lists.
- Implementation steps:
- Build a mapping from keys to new indices:
newIndexMap = {key1: 0, key2: 1, ...} - Traverse the old nodes to construct a new index array:
[newIndex1, newIndex2, ...] - Calculate the Longest Increasing Subsequence (LIS) of this array – these nodes have unchanged relative positions.
- Only need to move nodes not in the LIS, minimizing DOM operations.
- Build a mapping from keys to new indices:
- Advantage: Maximally reduces the number of node moves, improving update performance.
5. Source Code-Level Implementation Details
// Simplified `patchKeyedChildren` algorithm flow
function patchKeyedChildren(oldChildren, newChildren) {
// 1. Synchronized head pointer scan
let i = 0
while (i <= oldChildren.length - 1 && i <= newChildren.length - 1) {
if (sameVNode(oldChildren[i], newChildren[i])) {
patch(oldChildren[i], newChildren[i]) // Update node content
i++
} else {
break
}
}
// 2. Synchronized tail pointer scan
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
while (oldEnd >= i && newEnd >= i) {
if (sameVNode(oldChildren[oldEnd], newChildren[newEnd])) {
patch(oldChildren[oldEnd], newChildren[newEnd])
oldEnd--
newEnd--
} else {
break
}
}
// 3. New list has added nodes
if (i > oldEnd && i <= newEnd) {
for (let j = i; j <= newEnd; j++) {
mount(newChildren[j]) // Mount new nodes
}
return
}
// 4. Old list has removed nodes
if (i > newEnd && i <= oldEnd) {
for (let j = i; j <= oldEnd; j++) {
unmount(oldChildren[j]) // Unmount old nodes
}
return
}
// 5. Handling the disordered section (Key optimization)
const keyToNewIndexMap = new Map()
for (let j = i; j <= newEnd; j++) {
keyToNewIndexMap.set(newChildren[j].key, j)
}
// Find the Longest Increasing Subsequence to minimize move operations
const newIndexToOldIndexMap = new Array(newEnd - i + 1).fill(-1)
// ... Calculate LIS and perform minimal moves
}
6. Practical Application Suggestions
- Always provide a stable and meaningful
keyfor list items (avoid using index). - For complex lists, consider using virtual scrolling (rendering only the visible area).
- Understand the framework's update strategy to avoid unnecessary list re-renders.
This combination of optimizations enables the Virtual DOM to maintain high performance in list rendering scenarios while providing developers with a declarative programming experience.