Skip to content

Commit d3cb5b4

Browse files
committed
perf: improve performance of new diff algorithm
1 parent 885ddbf commit d3cb5b4

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)) {
@@ -937,94 +940,85 @@ export const vnode_diff = (
937940
}
938941
}
939942

940-
/**
941-
* This function is used to retrieve the child with the given key. If the child is not found, it
942-
* will return null.
943-
*
944-
* We will also collect all the keyed siblings found before the target key and add them to the
945-
* side buffer. This is done to optimize the search for the next child with the specified key.
946-
*
947-
* @param nodeName - The name of the node.
948-
* @param key - The key of the node.
949-
* @returns The child with the given key or null if not found.
950-
*/
951943
function retrieveChildWithKey(
952944
nodeName: string | null,
953945
key: string | null
954946
): ElementVNode | VirtualVNode | null {
955947
let vNodeWithKey: ElementVNode | VirtualVNode | null = null;
956-
957-
// if key is null we need to:
958-
// - if this is the first time fill the vSiblingsArray with all siblings
959-
// - if not then find the node we are interested in
960-
961-
if (key == null && vSiblingsArray != null) {
962-
for (let i = 0; i < vSiblingsArray.length; i += 2) {
963-
if (vSiblingsArray[i] === nodeName) {
964-
vNodeWithKey = vSiblingsArray![i + 1] as ElementVNode | VirtualVNode;
965-
vSiblingsArray.splice(i, 2);
966-
break;
948+
if (vSiblings === null) {
949+
// it is not materialized; so materialize it.
950+
vSiblings = new Map<string, VNode>();
951+
vSiblingsArray = [];
952+
let vNode = vCurrent;
953+
while (vNode) {
954+
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
955+
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
956+
if (vNodeWithKey === null && vKey == key && name == nodeName) {
957+
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
958+
} else {
959+
if (vKey === null) {
960+
vSiblingsArray.push(name, vNode);
961+
} else {
962+
// we only add the elements which we did not find yet.
963+
vSiblings.set(getSideBufferKey(name, vKey), vNode);
964+
}
965+
}
966+
vNode = vNode.nextSibling as VNode | null;
967+
}
968+
} else {
969+
if (key === null) {
970+
for (let i = 0; i < vSiblingsArray!.length; i += 2) {
971+
if (vSiblingsArray![i] === nodeName) {
972+
vNodeWithKey = vSiblingsArray![i + 1] as ElementVNode | VirtualVNode;
973+
vSiblingsArray!.splice(i, 2);
974+
break;
975+
}
976+
}
977+
} else {
978+
const siblingsKey = getSideBufferKey(nodeName, key);
979+
if (vSiblings.has(siblingsKey)) {
980+
vNodeWithKey = vSiblings.get(siblingsKey) as ElementVNode | VirtualVNode;
981+
vSiblings.delete(siblingsKey);
967982
}
968983
}
969-
return vNodeWithKey;
970984
}
971985

972-
const fillSiblingsArray = vSiblingsArray == null;
973-
let vNode = vCurrent;
974-
let foundTarget = false;
975-
let keyedSiblingsBeforeTarget: Array<{
976-
sideBufferKey: string;
977-
vNode: VNode;
978-
}> | null = null;
986+
collectSideBufferSiblings(vNodeWithKey);
979987

980-
while (vNode) {
981-
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
982-
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
988+
return vNodeWithKey;
989+
}
983990

984-
if (vNodeWithKey === null && vKey == key && name == nodeName) {
985-
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
986-
foundTarget = true;
987-
if (keyedSiblingsBeforeTarget && keyedSiblingsBeforeTarget.length > 0) {
991+
function collectSideBufferSiblings(targetNode: VNode | null): void {
992+
if (!targetNode) {
993+
if (vCurrent) {
994+
const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null;
995+
const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
996+
if (vKey != null) {
997+
const sideBufferKey = getSideBufferKey(name, vKey);
988998
vSideBuffer ||= new Map();
989-
// Add all collected keyed siblings to side buffer now that we found the target
990-
for (const sibling of keyedSiblingsBeforeTarget) {
991-
vSideBuffer.set(sibling.sideBufferKey, sibling.vNode);
992-
}
993-
}
994-
if (!fillSiblingsArray) {
995-
break;
996-
}
997-
} else {
998-
if (vKey == null) {
999-
if (fillSiblingsArray) {
1000-
// Unkeyed sibling - add to siblings array
1001-
vSiblingsArray ||= [];
1002-
vSiblingsArray.push(name, vNode);
1003-
}
1004-
} else {
1005-
if (!foundTarget) {
1006-
keyedSiblingsBeforeTarget ||= [];
1007-
const sideBufferKey = getSideBufferKey(name, vKey);
1008-
// Collect keyed sibling found before target
1009-
keyedSiblingsBeforeTarget.push({ sideBufferKey, vNode });
1010-
}
999+
vSideBuffer.set(sideBufferKey, vCurrent);
1000+
vSiblings?.delete(sideBufferKey);
10111001
}
10121002
}
10131003

1014-
vNode = vNode.nextSibling as VNode | null;
1004+
return;
10151005
}
10161006

1017-
// add current to the side buffer if it is not the target
1018-
if (!foundTarget && vCurrent) {
1019-
const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null;
1020-
const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$);
1007+
// Walk from vCurrent up to the target node and collect all keyed siblings
1008+
let vNode = vCurrent;
1009+
while (vNode && vNode !== targetNode) {
1010+
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
1011+
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
1012+
10211013
if (vKey != null) {
10221014
const sideBufferKey = getSideBufferKey(name, vKey);
10231015
vSideBuffer ||= new Map();
1024-
vSideBuffer.set(sideBufferKey, vCurrent);
1016+
vSideBuffer.set(sideBufferKey, vNode);
1017+
vSiblings?.delete(sideBufferKey);
10251018
}
1019+
1020+
vNode = vNode.nextSibling as VNode | null;
10261021
}
1027-
return vNodeWithKey;
10281022
}
10291023

10301024
function getSideBufferKey(nodeName: string | null, key: string): string;
@@ -1066,6 +1060,7 @@ export const vnode_diff = (
10661060
): any {
10671061
// 1) Try to find the node among upcoming siblings
10681062
vNewNode = retrieveChildWithKey(nodeName, lookupKey);
1063+
10691064
if (vNewNode) {
10701065
vCurrent = vNewNode;
10711066
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)