Skip to content

Commit 4fc7667

Browse files
committed
perf: improve performance of new diff algorithm
1 parent ff61ff1 commit 4fc7667

File tree

2 files changed

+86
-67
lines changed

2 files changed

+86
-67
lines changed

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 62 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const vnode_diff = (
123123
/// and is not connected to the tree.
124124
let vNewNode: VNode | null = null;
125125

126+
let vSiblings: Map<string, VNode> | null = null;
126127
/// The array even indices will contains keys and odd indices the non keyed siblings.
127128
let vSiblingsArray: Array<string | VNode | null> | null = null;
128129

@@ -319,6 +320,7 @@ export const vnode_diff = (
319320
if (descendVNode) {
320321
assertDefined(vCurrent || vNewNode, 'Expecting vCurrent to be defined.');
321322
vSideBuffer = null;
323+
vSiblings = null;
322324
vSiblingsArray = null;
323325
vParent = (vNewNode || vCurrent!) as ElementVNode | VirtualVNode;
324326
vCurrent = vnode_getFirstChild(vParent);
@@ -331,6 +333,7 @@ export const vnode_diff = (
331333
const descendVNode = stack.pop(); // boolean: descendVNode
332334
if (descendVNode) {
333335
vSideBuffer = stack.pop();
336+
vSiblings = stack.pop();
334337
vSiblingsArray = stack.pop();
335338
vNewNode = stack.pop();
336339
vCurrent = stack.pop();
@@ -346,7 +349,7 @@ export const vnode_diff = (
346349
function stackPush(children: JSXChildren, descendVNode: boolean) {
347350
stack.push(jsxChildren, jsxIdx, jsxCount, jsxValue);
348351
if (descendVNode) {
349-
stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSideBuffer);
352+
stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSiblings, vSideBuffer);
350353
}
351354
stack.push(descendVNode);
352355
if (Array.isArray(children)) {
@@ -926,94 +929,85 @@ export const vnode_diff = (
926929
}
927930
}
928931

929-
/**
930-
* This function is used to retrieve the child with the given key. If the child is not found, it
931-
* will return null.
932-
*
933-
* We will also collect all the keyed siblings found before the target key and add them to the
934-
* side buffer. This is done to optimize the search for the next child with the specified key.
935-
*
936-
* @param nodeName - The name of the node.
937-
* @param key - The key of the node.
938-
* @returns The child with the given key or null if not found.
939-
*/
940932
function retrieveChildWithKey(
941933
nodeName: string | null,
942934
key: string | null
943935
): ElementVNode | VirtualVNode | null {
944936
let vNodeWithKey: ElementVNode | VirtualVNode | null = null;
945-
946-
// if key is null we need to:
947-
// - if this is the first time fill the vSiblingsArray with all siblings
948-
// - if not then find the node we are interested in
949-
950-
if (key == null && vSiblingsArray != null) {
951-
for (let i = 0; i < vSiblingsArray.length; i += 2) {
952-
if (vSiblingsArray[i] === nodeName) {
953-
vNodeWithKey = vSiblingsArray![i + 1] as ElementVNode | VirtualVNode;
954-
vSiblingsArray.splice(i, 2);
955-
break;
937+
if (vSiblings === null) {
938+
// it is not materialized; so materialize it.
939+
vSiblings = new Map<string, VNode>();
940+
vSiblingsArray = [];
941+
let vNode = vCurrent;
942+
while (vNode) {
943+
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
944+
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
945+
if (vNodeWithKey === null && vKey == key && name == nodeName) {
946+
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
947+
} else {
948+
if (vKey === null) {
949+
vSiblingsArray.push(name, vNode);
950+
} else {
951+
// we only add the elements which we did not find yet.
952+
vSiblings.set(getSideBufferKey(name, vKey), vNode);
953+
}
954+
}
955+
vNode = vNode.nextSibling as VNode | null;
956+
}
957+
} else {
958+
if (key === null) {
959+
for (let i = 0; i < vSiblingsArray!.length; i += 2) {
960+
if (vSiblingsArray![i] === nodeName) {
961+
vNodeWithKey = vSiblingsArray![i + 1] as ElementVNode | VirtualVNode;
962+
vSiblingsArray!.splice(i, 2);
963+
break;
964+
}
965+
}
966+
} else {
967+
const siblingsKey = getSideBufferKey(nodeName, key);
968+
if (vSiblings.has(siblingsKey)) {
969+
vNodeWithKey = vSiblings.get(siblingsKey) as ElementVNode | VirtualVNode;
970+
vSiblings.delete(siblingsKey);
956971
}
957972
}
958-
return vNodeWithKey;
959973
}
960974

961-
const fillSiblingsArray = vSiblingsArray == null;
962-
let vNode = vCurrent;
963-
let foundTarget = false;
964-
let keyedSiblingsBeforeTarget: Array<{
965-
sideBufferKey: string;
966-
vNode: VNode;
967-
}> | null = null;
975+
collectSideBufferSiblings(vNodeWithKey);
968976

969-
while (vNode) {
970-
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
971-
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
977+
return vNodeWithKey;
978+
}
972979

973-
if (vNodeWithKey === null && vKey == key && name == nodeName) {
974-
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
975-
foundTarget = true;
976-
if (keyedSiblingsBeforeTarget && keyedSiblingsBeforeTarget.length > 0) {
980+
function collectSideBufferSiblings(targetNode: VNode | null): void {
981+
if (!targetNode) {
982+
if (vCurrent) {
983+
const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null;
984+
const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
985+
if (vKey != null) {
986+
const sideBufferKey = getSideBufferKey(name, vKey);
977987
vSideBuffer ||= new Map();
978-
// Add all collected keyed siblings to side buffer now that we found the target
979-
for (const sibling of keyedSiblingsBeforeTarget) {
980-
vSideBuffer.set(sibling.sideBufferKey, sibling.vNode);
981-
}
982-
}
983-
if (!fillSiblingsArray) {
984-
break;
985-
}
986-
} else {
987-
if (vKey == null) {
988-
if (fillSiblingsArray) {
989-
// Unkeyed sibling - add to siblings array
990-
vSiblingsArray ||= [];
991-
vSiblingsArray.push(name, vNode);
992-
}
993-
} else {
994-
if (!foundTarget) {
995-
keyedSiblingsBeforeTarget ||= [];
996-
const sideBufferKey = getSideBufferKey(name, vKey);
997-
// Collect keyed sibling found before target
998-
keyedSiblingsBeforeTarget.push({ sideBufferKey, vNode });
999-
}
988+
vSideBuffer.set(sideBufferKey, vCurrent);
989+
vSiblings?.delete(sideBufferKey);
1000990
}
1001991
}
1002992

1003-
vNode = vNode.nextSibling as VNode | null;
993+
return;
1004994
}
1005995

1006-
// add current to the side buffer if it is not the target
1007-
if (!foundTarget && vCurrent) {
1008-
const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null;
1009-
const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
996+
// Walk from vCurrent up to the target node and collect all keyed siblings
997+
let vNode = vCurrent;
998+
while (vNode && vNode !== targetNode) {
999+
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
1000+
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
1001+
10101002
if (vKey != null) {
10111003
const sideBufferKey = getSideBufferKey(name, vKey);
10121004
vSideBuffer ||= new Map();
1013-
vSideBuffer.set(sideBufferKey, vCurrent);
1005+
vSideBuffer.set(sideBufferKey, vNode);
1006+
vSiblings?.delete(sideBufferKey);
10141007
}
1008+
1009+
vNode = vNode.nextSibling as VNode | null;
10151010
}
1016-
return vNodeWithKey;
10171011
}
10181012

10191013
function getSideBufferKey(nodeName: string | null, key: string): string;
@@ -1055,6 +1049,7 @@ export const vnode_diff = (
10551049
): any {
10561050
// 1) Try to find the node among upcoming siblings
10571051
vNewNode = retrieveChildWithKey(nodeName, lookupKey);
1052+
10581053
if (vNewNode) {
10591054
vCurrent = vNewNode;
10601055
vNewNode = null;

packages/qwik/src/core/client/vnode-diff.unit.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,30 @@ describe('vNode-diff', () => {
431431
expect(b1).toBe(selectB1());
432432
expect(b2).toBe(selectB2());
433433
});
434+
435+
it('should remove or add keyed nodes', () => {
436+
const { vNode, vParent, container } = vnode_fromJSX(
437+
_jsxSorted(
438+
'test',
439+
{},
440+
null,
441+
[_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, null)],
442+
0,
443+
'KA_6'
444+
)
445+
);
446+
const test = _jsxSorted(
447+
'test',
448+
{},
449+
null,
450+
[_jsxSorted('b', {}, null, '2', 0, null), _jsxSorted('b', {}, null, '2', 0, '2')],
451+
0,
452+
'KA_6'
453+
);
454+
vnode_diff(container, test, vParent, null);
455+
vnode_applyJournal(container.$journal$);
456+
expect(vNode).toMatchVDOM(test);
457+
});
434458
});
435459
describe('fragments', () => {
436460
it('should not rerender signal wrapper fragment', async () => {

0 commit comments

Comments
 (0)