From 6cd1ccd74b25018d7fd5f4c7802534eb119540b7 Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 22 May 2025 20:18:49 +0200 Subject: [PATCH] fix: serialize less vnode data --- packages/qwik/src/core/client/vnode-diff.ts | 14 +++- .../src/core/shared/shared-serialization.ts | 19 +++-- .../qwik/src/core/tests/component.spec.tsx | 70 +++++++++++++++++++ .../src/core/tests/render-namespace.spec.tsx | 2 +- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index c8692a2dfbd..1fa22503ba1 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -1237,7 +1237,12 @@ function getComponentHash(vNode: VNode | null, getObject: (id: string) => any): function Projection() {} function propsDiffer(src: Record, dst: Record): boolean { - if (!src || !dst) { + const srcEmpty = isPropsEmpty(src); + const dstEmpty = isPropsEmpty(dst); + if (srcEmpty && dstEmpty) { + return false; + } + if (srcEmpty || dstEmpty) { return true; } let srcKeys = removePropsKeys(Object.keys(src), ['children', QBackRefs]); @@ -1257,6 +1262,13 @@ function propsDiffer(src: Record, dst: Record): boolea return false; } +function isPropsEmpty(props: Record): boolean { + if (!props) { + return true; + } + return Object.keys(props).length === 0; +} + function removePropsKeys(keys: string[], propKeys: string[]): string[] { for (let i = propKeys.length - 1; i >= 0; i--) { const propKey = propKeys[i]; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 37eb46db9e0..d4667e91a8d 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -38,7 +38,7 @@ import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types'; import { _CONST_PROPS, _VAR_PROPS } from './utils/constants'; import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; -import { ELEMENT_ID } from './utils/markers'; +import { ELEMENT_ID, QBackRefs } from './utils/markers'; import { isPromise } from './utils/promises'; import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; import { @@ -843,12 +843,16 @@ function $discoverRoots$( const isSsrAttrs = (value: number | SsrAttrs): value is SsrAttrs => Array.isArray(value) && value.length > 0; -const discoverValuesForVNodeData = (vnodeData: VNodeData, callback: (value: unknown) => void) => { +const discoverValuesForVNodeData = ( + vnodeData: VNodeData, + callback: (value: unknown) => void, + filter: (key: string, value: unknown) => boolean = (_, attrValue) => typeof attrValue === 'string' +) => { for (const value of vnodeData) { if (isSsrAttrs(value)) { for (let i = 1; i < value.length; i += 2) { const attrValue = value[i]; - if (typeof attrValue === 'string') { + if (filter(value[i - 1] as string, attrValue)) { continue; } callback(attrValue); @@ -1198,7 +1202,14 @@ async function serialize(serializationContext: SerializationContext): Promise $addRoot$(vNodeDataValue)); + discoverValuesForVNodeData( + vNodeData, + (vNodeDataValue) => { + $addRoot$(vNodeDataValue); + }, + (key, value) => typeof value === 'string' && key !== QBackRefs + ); + vNodeData[0] |= VNodeDataFlag.SERIALIZE; } } diff --git a/packages/qwik/src/core/tests/component.spec.tsx b/packages/qwik/src/core/tests/component.spec.tsx index 1aa21550ed6..d20a5836649 100644 --- a/packages/qwik/src/core/tests/component.spec.tsx +++ b/packages/qwik/src/core/tests/component.spec.tsx @@ -301,6 +301,76 @@ describe.each([ ); }); + it('should not rerender component with empty props', async () => { + (globalThis as any).componentExecuted = []; + const Component1 = component$>(() => { + (globalThis as any).componentExecuted.push('Component1'); + return
; + }); + const Parent = component$(() => { + (globalThis as any).componentExecuted.push('Parent'); + const show = useSignal(true); + return ( +
(show.value = !show.value)}> + {show.value && } + +
+ ); + }); + const { vNode, container } = await render(, { debug }); + expect((globalThis as any).componentExecuted).toEqual(['Parent', 'Component1', 'Component1']); + expect(vNode).toMatchVDOM( + +
+ +
+
+ +
+
+
+
+ ); + await trigger(container.element, 'main.parent', 'click'); + expect((globalThis as any).componentExecuted).toEqual([ + 'Parent', + 'Component1', + 'Component1', + 'Parent', + ]); + expect(vNode).toMatchVDOM( + +
+ {''} + +
+
+
+
+ ); + await trigger(container.element, 'main.parent', 'click'); + expect((globalThis as any).componentExecuted).toEqual([ + 'Parent', + 'Component1', + 'Component1', + 'Parent', + 'Parent', + 'Component1', + ]); + expect(vNode).toMatchVDOM( + +
+ +
+
+ +
+
+
+
+ ); + }); + it('should remove children from component$', async () => { const log: string[] = []; const MyComp = component$((props: any) => { diff --git a/packages/qwik/src/core/tests/render-namespace.spec.tsx b/packages/qwik/src/core/tests/render-namespace.spec.tsx index 9fae64a094e..f852a55c6e2 100644 --- a/packages/qwik/src/core/tests/render-namespace.spec.tsx +++ b/packages/qwik/src/core/tests/render-namespace.spec.tsx @@ -227,7 +227,7 @@ describe.each([ - +