Skip to content

Commit 0bdbd8f

Browse files
authored
Merge pull request #8171 from QwikDev/v2-optimize-attr-diff
feat: used optimized method for setting changed attribute value
2 parents c3221c2 + be5d399 commit 0bdbd8f

File tree

1 file changed

+57
-21
lines changed

1 file changed

+57
-21
lines changed

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

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {
5252
QDefaultSlot,
5353
QSlot,
5454
QTemplate,
55-
Q_PREFIX,
5655
dangerouslySetInnerHTML,
5756
} from '../shared/utils/markers';
5857
import { isPromise, retryOnPromise } from '../shared/utils/promises';
@@ -66,6 +65,7 @@ import type { DomContainer } from './dom-container';
6665
import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types';
6766
import { mapApp_findIndx, mapArray_set } from './util-mapArray';
6867
import {
68+
VNodeJournalOpCode,
6969
vnode_ensureElementInflated,
7070
vnode_getDomParentVNode,
7171
vnode_getElementName,
@@ -90,7 +90,7 @@ import {
9090
vnode_walkVNode,
9191
type VNodeJournal,
9292
} from './vnode';
93-
import type { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl';
93+
import { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl';
9494
import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace';
9595
import { cleanupDestroyable } from '../use/utils/destroyable';
9696
import { SignalImpl } from '../reactive-primitives/impl/signal-impl';
@@ -858,15 +858,44 @@ export const vnode_diff = (
858858
let dstIdx = 0;
859859
let patchEventDispatch = false;
860860

861-
const setAttribute = (key: string, value: any, vHost: ElementVNode) => {
862-
vHost.setAttr(
863-
key,
864-
value != null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null,
865-
journal
866-
);
861+
/**
862+
* Optimized setAttribute that bypasses redundant checks when we already know:
863+
*
864+
* - The index in dstAttrs (no need for binary search)
865+
* - The vnode is ElementVNode (no instanceof check)
866+
* - The value has changed (no comparison needed)
867+
*/
868+
const setAttributeDirect = (
869+
vnode: ElementVNode,
870+
key: string,
871+
value: any,
872+
dstIdx: number,
873+
isNewKey: boolean
874+
) => {
875+
const serializedValue =
876+
value != null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null;
877+
878+
if (isNewKey) {
879+
// Adding new key - splice into sorted position
880+
if (serializedValue != null) {
881+
(dstAttrs as any).splice(dstIdx, 0, key, serializedValue);
882+
journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue);
883+
}
884+
} else {
885+
// Updating or removing existing key at dstIdx
886+
if (serializedValue != null) {
887+
// Update existing value
888+
(dstAttrs as any)[dstIdx + 1] = serializedValue;
889+
journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue);
890+
} else {
891+
// Remove key (value is null)
892+
dstAttrs.splice(dstIdx, 2);
893+
journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, null);
894+
}
895+
}
867896
};
868897

869-
const record = (key: string, value: any) => {
898+
const record = (key: string, value: any, dstIdx: number, isNewKey: boolean) => {
870899
if (key.startsWith(':')) {
871900
vnode.setProp(key, value);
872901
return;
@@ -921,15 +950,21 @@ export const vnode_diff = (
921950
}
922951

923952
if (isPromise(value)) {
953+
// For async values, we can't use the known index since it will be stale by the time
954+
// the promise resolves. Do a binary search to find the current index.
924955
const vHost = vnode as ElementVNode;
925-
const attributePromise = value.then((resolvedValue) =>
926-
setAttribute(key, resolvedValue, vHost)
927-
);
956+
const attributePromise = value.then((resolvedValue) => {
957+
const idx = mapApp_findIndx(dstAttrs, key, 0);
958+
const isNewKey = idx < 0;
959+
const currentDstIdx = isNewKey ? idx ^ -1 : idx;
960+
setAttributeDirect(vHost, key, resolvedValue, currentDstIdx, isNewKey);
961+
});
928962
asyncAttributePromises.push(attributePromise);
929963
return;
930964
}
931965

932-
setAttribute(key, value, vnode);
966+
// Always use optimized direct path - we know the index from the merge algorithm
967+
setAttributeDirect(vnode, key, value, dstIdx, isNewKey);
933968
};
934969

935970
const recordJsxEvent = (key: string, value: any) => {
@@ -938,7 +973,8 @@ export const vnode_diff = (
938973
const [scope, eventName] = data;
939974
const scopedEvent = getScopedEventName(scope, eventName);
940975
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);
941-
record(':' + scopedEvent, value);
976+
// Pass dummy index values since ':' prefixed keys take early return via setProp
977+
record(':' + scopedEvent, value, 0, false);
942978
// register an event for qwik loader (window/document prefixed with '-')
943979
registerQwikLoaderEvent(loaderScopedEvent);
944980
patchEventDispatch = true;
@@ -951,8 +987,8 @@ export const vnode_diff = (
951987
const srcKey = srcIdx < srcAttrs.length ? (srcAttrs[srcIdx] as string) : undefined;
952988
const dstKey = dstIdx < dstAttrs.length ? (dstAttrs[dstIdx] as string) : undefined;
953989

954-
// Skip special keys in destination (HANDLER_PREFIX, Q_PREFIX)
955-
if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey?.startsWith(Q_PREFIX)) {
990+
// Skip special keys in destination HANDLER_PREFIX
991+
if (dstKey?.startsWith(HANDLER_PREFIX)) {
956992
dstIdx += 2; // skip key and value
957993
continue;
958994
}
@@ -963,7 +999,7 @@ export const vnode_diff = (
963999
// HTML event attributes are immutable and not removed from DOM
9641000
dstIdx += 2; // skip key and value
9651001
} else {
966-
record(dstKey!, null);
1002+
record(dstKey!, null, dstIdx, false);
9671003
// After removal, dstAttrs shrinks by 2, so don't advance dstIdx
9681004
}
9691005
} else if (dstKey === undefined) {
@@ -972,7 +1008,7 @@ export const vnode_diff = (
9721008
if (isHtmlAttributeAnEventName(srcKey)) {
9731009
recordJsxEvent(srcKey, srcValue);
9741010
} else {
975-
record(srcKey, srcValue);
1011+
record(srcKey, srcValue, dstIdx, true);
9761012
}
9771013
srcIdx += 2; // skip key and value
9781014
// After addition, dstAttrs grows by 2 at sorted position, advance dstIdx
@@ -986,7 +1022,7 @@ export const vnode_diff = (
9861022
if (isEventHandler) {
9871023
recordJsxEvent(srcKey, srcValue);
9881024
} else {
989-
record(srcKey, srcValue);
1025+
record(srcKey, srcValue, dstIdx, false);
9901026
}
9911027
} else if (isEventHandler && !vnode.element.qDispatchEvent) {
9921028
// Special case: add event handlers after resume
@@ -1001,7 +1037,7 @@ export const vnode_diff = (
10011037
if (isHtmlAttributeAnEventName(srcKey)) {
10021038
recordJsxEvent(srcKey, srcValue);
10031039
} else {
1004-
record(srcKey, srcValue);
1040+
record(srcKey, srcValue, dstIdx, true);
10051041
}
10061042
srcIdx += 2; // skip key and value
10071043
// After addition, dstAttrs grows at sorted position (before dstIdx), advance dstIdx
@@ -1012,7 +1048,7 @@ export const vnode_diff = (
10121048
// HTML event attributes are immutable and not removed from DOM
10131049
dstIdx += 2; // skip key and value
10141050
} else {
1015-
record(dstKey, null);
1051+
record(dstKey, null, dstIdx, false);
10161052
// After removal, dstAttrs shrinks at dstIdx, so don't advance dstIdx
10171053
}
10181054
}

0 commit comments

Comments
 (0)