@@ -52,7 +52,6 @@ import {
5252 QDefaultSlot ,
5353 QSlot ,
5454 QTemplate ,
55- Q_PREFIX ,
5655 dangerouslySetInnerHTML ,
5756} from '../shared/utils/markers' ;
5857import { isPromise , retryOnPromise } from '../shared/utils/promises' ;
@@ -66,6 +65,7 @@ import type { DomContainer } from './dom-container';
6665import { VNodeFlags , type ClientAttrs , type ClientContainer } from './types' ;
6766import { mapApp_findIndx , mapArray_set } from './util-mapArray' ;
6867import {
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' ;
9494import { getAttributeNamespace , getNewElementNamespaceData } from './vnode-namespace' ;
9595import { cleanupDestroyable } from '../use/utils/destroyable' ;
9696import { 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