Skip to content

fix: serialize less vnode data #7636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-pears-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: serialize less vnode data
65 changes: 65 additions & 0 deletions packages/docs/src/components/on-this-page/on-this-page-more.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { component$ } from '@qwik.dev/core';
import { EditIcon } from '../svgs/edit-icon';
import { AlertIcon } from '../svgs/alert-icon';
import { ChatIcon } from '../svgs/chat-icon';
import { GithubLogo } from '../svgs/github-logo';
import { TwitterLogo } from '../svgs/twitter-logo';

type OnThisPageMoreProps = {
theme: 'light' | 'dark' | 'auto';
editUrl: string;
};

export const OnThisPageMore = component$<OnThisPageMoreProps>(({ theme, editUrl }) => {
const OnThisPageMore = [
{
href: editUrl,
text: 'Edit this Page',
icon: EditIcon,
},
{
href: 'https://github.com/QwikDev/qwik/issues/new/choose',
text: 'Create an issue',
icon: AlertIcon,
},
{
href: 'https://qwik.dev/chat',
text: 'Join our community',
icon: ChatIcon,
},
{
href: 'https://github.com/QwikDev/qwik',
text: 'GitHub',
icon: GithubLogo,
},
{
href: 'https://twitter.com/QwikDev',
text: '@QwikDev',
icon: TwitterLogo,
},
];
return (
<>
<h6>More</h6>
<ul class="px-2 font-medium text-[var(--interactive-text-color)]">
{OnThisPageMore.map((el, index) => {
return (
<li
class={`${
theme === 'light'
? 'hover:bg-[var(--qwik-light-blue)]'
: 'hover:bg-[var(--on-this-page-hover-bg-color)]'
} rounded-lg`}
key={`more-items-on-this-page-${index}`}
>
<a class="more-item" href={el.href} rel="noopener" target="_blank">
{el.icon && <el.icon width={20} height={20} />}
<span>{el.text}</span>
</a>
</li>
);
})}
</ul>
</>
);
});
58 changes: 2 additions & 56 deletions packages/docs/src/components/on-this-page/on-this-page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { $, component$, useContext, useOnDocument, useSignal, useStyles$ } from '@qwik.dev/core';
import { useContent, useLocation } from '@qwik.dev/router';
import { GlobalStore } from '../../context';
import { AlertIcon } from '../svgs/alert-icon';
import { ChatIcon } from '../svgs/chat-icon';
import { EditIcon } from '../svgs/edit-icon';
import { GithubLogo } from '../svgs/github-logo';
import { TwitterLogo } from '../svgs/twitter-logo';
import styles from './on-this-page.css?inline';
import { OnThisPageMore } from './on-this-page-more';

const QWIK_GROUP = [
'components',
Expand Down Expand Up @@ -117,39 +113,9 @@ export const OnThisPage = component$(() => {
const contentHeadings = headings?.filter((h) => h.level <= 3) || [];

const { url } = useLocation();

const githubEditRoute = makeEditPageUrl(url.pathname);

const editUrl = `https://github.com/QwikDev/qwik/edit/main/packages/docs/src/routes/${githubEditRoute}/index.mdx`;

const OnThisPageMore = [
{
href: editUrl,
text: 'Edit this Page',
icon: EditIcon,
},
{
href: 'https://github.com/QwikDev/qwik/issues/new/choose',
text: 'Create an issue',
icon: AlertIcon,
},
{
href: 'https://qwik.dev/chat',
text: 'Join our community',
icon: ChatIcon,
},
{
href: 'https://github.com/QwikDev/qwik',
text: 'GitHub',
icon: GithubLogo,
},
{
href: 'https://twitter.com/QwikDev',
text: '@QwikDev',
icon: TwitterLogo,
},
];

const useActiveItem = (itemIds: string[]) => {
const activeId = useSignal<string | null>(null);
useOnDocument(
Expand Down Expand Up @@ -214,29 +180,9 @@ export const OnThisPage = component$(() => {
</li>
))}
</ul>
<OnThisPageMore theme={theme.theme} editUrl={editUrl} />
</>
) : null}

<h6>More</h6>
<ul class="px-2 font-medium text-[var(--interactive-text-color)]">
{OnThisPageMore.map((el, index) => {
return (
<li
class={`${
theme.theme === 'light'
? 'hover:bg-[var(--qwik-light-blue)]'
: 'hover:bg-[var(--on-this-page-hover-bg-color)]'
} rounded-lg`}
key={`more-items-on-this-page-${index}`}
>
<a class="more-item" href={el.href} rel="noopener" target="_blank">
<el.icon width={20} height={20} />
<span>{el.text}</span>
</a>
</li>
);
})}
</ul>
</aside>
);
});
7 changes: 3 additions & 4 deletions packages/qwik/src/core/qwik.core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,14 +892,13 @@ export abstract class _SharedContainer implements Container {
// (undocumented)
serializationCtxFactory(NodeConstructor: {
new (...rest: any[]): {
nodeType: number;
id: string;
__brand__: 'SsrNode';
};
} | null, DomRefConstructor: {
new (...rest: any[]): {
$ssrNode$: ISsrNode;
__brand__: 'DomRef';
};
} | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter, prepVNodeData?: (vNode: any) => void): SerializationContext;
} | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter): SerializationContext;
// (undocumented)
abstract setContext<T>(host: HostElement, context: ContextId<T>, value: T): void;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/reactive-primitives/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ export function getSubscriber(
}

function isSsrNode(value: any): value is ISsrNode {
return '__brand__' in value && 'currentComponentNode' in value;
return '__brand__' in value && value.__brand__ === 'SsrNode';
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ export const ssrNodeDocumentPosition = (a: ISsrNode, b: ISsrNode): -1 | 0 | 1 =>
let bDepth = -1;
while (a) {
const ssrNode = (aSsrNodePath[++aDepth] = a);
a = ssrNode.currentComponentNode!;
a = ssrNode.parentSsrNode!;
}
while (b) {
const ssrNode = (bSsrNodePath[++bDepth] = b);
b = ssrNode.currentComponentNode!;
b = ssrNode.parentSsrNode!;
}

while (aDepth >= 0 && bDepth >= 0) {
Expand Down
12 changes: 5 additions & 7 deletions packages/qwik/src/core/shared/shared-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { trackSignalAndAssignHost } from '../use/use-core';
import { version } from '../version';
import type { SubscriptionData } from '../reactive-primitives/subscription-data';
import type { Signal } from '../reactive-primitives/signal.public';
import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types';
import type { StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types';
import type { Scheduler } from './scheduler';
import { createScheduler } from './scheduler';
import { createSerializationContext, type SerializationContext } from './shared-serialization';
Expand Down Expand Up @@ -51,14 +51,13 @@ export abstract class _SharedContainer implements Container {

serializationCtxFactory(
NodeConstructor: {
new (...rest: any[]): { nodeType: number; id: string };
new (...rest: any[]): { __brand__: 'SsrNode' };
} | null,
DomRefConstructor: {
new (...rest: any[]): { $ssrNode$: ISsrNode };
new (...rest: any[]): { __brand__: 'DomRef' };
} | null,
symbolToChunkResolver: SymbolToChunkResolver,
writer?: StreamWriter,
prepVNodeData?: (vNode: any) => void
writer?: StreamWriter
): SerializationContext {
return createSerializationContext(
NodeConstructor,
Expand All @@ -67,8 +66,7 @@ export abstract class _SharedContainer implements Container {
this.getHostProp.bind(this),
this.setHostProp.bind(this),
this.$storeProxyMap$,
writer,
prepVNodeData
writer
);
}

Expand Down
51 changes: 32 additions & 19 deletions packages/qwik/src/core/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, ELEMENT_PROPS, QBackRefs } from './utils/markers';
import { isPromise } from './utils/promises';
import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils';
import {
Expand Down Expand Up @@ -601,9 +601,8 @@ export function inflateQRL(container: DeserializeContainer, qrl: QRLInternal<any

/** A selection of attributes of the real thing */
type SsrNode = {
nodeType: number;
id: string;
childrenVNodeData: VNodeData[] | null;
children: ISsrNode[] | null;
vnodeData: VNodeData;
[_EFFECT_BACK_REF]: Map<EffectProperty | string, EffectSubscription> | null;
};
Expand Down Expand Up @@ -679,7 +678,6 @@ export interface SerializationContext {

$getProp$: (obj: any, prop: string) => any;
$setProp$: (obj: any, prop: string, value: any) => void;
$prepVNodeData$?: (vNodeData: VNodeData) => void;
}

export const createSerializationContext = (
Expand All @@ -690,19 +688,17 @@ export const createSerializationContext = (
* server will not know what to do with them.
*/
NodeConstructor: {
new (...rest: any[]): { nodeType: number; id: string };
new (...rest: any[]): { __brand__: 'SsrNode' };
} | null,
/** DomRef constructor, for instanceof checks. */
DomRefConstructor: {
new (...rest: any[]): { $ssrNode$: ISsrNode };
new (...rest: any[]): { __brand__: 'DomRef' };
} | null,
symbolToChunkResolver: SymbolToChunkResolver,
getProp: (obj: any, prop: string) => any,
setProp: (obj: any, prop: string, value: any) => void,
storeProxyMap: ObjToProxyMap,
writer?: StreamWriter,
// temporary until we serdes the vnode data here
prepVNodeData?: (vNodeData: VNodeData) => void
writer?: StreamWriter
): SerializationContext => {
if (!writer) {
const buffer: string[] = [];
Expand Down Expand Up @@ -763,9 +759,10 @@ export const createSerializationContext = (
return seen.$rootIndex$;
};

const isSsrNode = (NodeConstructor ? (obj) => obj instanceof NodeConstructor : () => false) as (
obj: unknown
) => obj is SsrNode;
const isSsrNode = (
NodeConstructor ? (obj) => obj instanceof NodeConstructor : ((() => false) as any)
) as (obj: unknown) => obj is SsrNode;

isDomRef = (
DomRefConstructor ? (obj) => obj instanceof DomRefConstructor : ((() => false) as any)
) as (obj: unknown) => obj is DomRef;
Expand Down Expand Up @@ -816,7 +813,6 @@ export const createSerializationContext = (
$storeProxyMap$: storeProxyMap,
$getProp$: getProp,
$setProp$: setProp,
$prepVNodeData$: prepVNodeData,
$pathMap$: rootsPathMap,
};
};
Expand Down Expand Up @@ -847,8 +843,14 @@ const discoverValuesForVNodeData = (vnodeData: VNodeData, callback: (value: unkn
for (const value of vnodeData) {
if (isSsrAttrs(value)) {
for (let i = 1; i < value.length; i += 2) {
const keyValue = value[i - 1];
const attrValue = value[i];
if (typeof attrValue === 'string') {
if (
typeof attrValue === 'string' ||
// skip empty props
(keyValue === ELEMENT_PROPS &&
Object.keys(attrValue as Record<string, unknown>).length === 0)
) {
continue;
}
callback(attrValue);
Expand Down Expand Up @@ -1192,14 +1194,25 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
output(TypeIds.VNode, value.id);
const vNodeData = value.vnodeData;
if (vNodeData) {
serializationContext.$prepVNodeData$?.(vNodeData);
discoverValuesForVNodeData(vNodeData, (vNodeDataValue) => $addRoot$(vNodeDataValue));
vNodeData[0] |= VNodeDataFlag.SERIALIZE;
}
if (value.childrenVNodeData) {
for (const vNodeData of value.childrenVNodeData) {
discoverValuesForVNodeData(vNodeData, (vNodeDataValue) => $addRoot$(vNodeDataValue));
vNodeData[0] |= VNodeDataFlag.SERIALIZE;
if (value.children) {
// can be static, but we need to save vnode data structure + discover the back refs
for (const child of value.children) {
const childVNodeData = child.vnodeData;
if (childVNodeData) {
// add all back refs to the roots
for (const value of childVNodeData) {
if (isSsrAttrs(value)) {
const backRefKeyIndex = value.findIndex((v) => v === QBackRefs);
if (backRefKeyIndex !== -1) {
$addRoot$(value[backRefKeyIndex + 1]);
}
}
}
childVNodeData[0] |= VNodeDataFlag.SERIALIZE;
}
}
}
} else if (typeof FormData !== 'undefined' && value instanceof FormData) {
Expand Down
4 changes: 2 additions & 2 deletions packages/qwik/src/core/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export interface Container {
ensureProjectionResolved(host: HostElement): void;
serializationCtxFactory(
NodeConstructor: {
new (...rest: any[]): { nodeType: number; id: string };
new (...rest: any[]): { __brand__: 'SsrNode' };
} | null,
DomRefConstructor: {
new (...rest: any[]): { $ssrNode$: ISsrNode };
new (...rest: any[]): { __brand__: 'DomRef' };
} | null,
symbolToChunkResolver: SymbolToChunkResolver,
writer?: StreamWriter
Expand Down
4 changes: 2 additions & 2 deletions packages/qwik/src/core/ssr/ssr-render-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const applyInlineComponent = (
inlineComponentFunction: OnRenderFn<any>,
jsx: JSXNode
) => {
const host = ssr.getLastNode();
const host = ssr.getOrCreateLastNode();
return executeComponent(ssr, host, componentHost, inlineComponentFunction, jsx.props);
};

Expand All @@ -23,7 +23,7 @@ export const applyQwikComponentBody = (
jsx: JSXNode,
component: Component
): ValueOrPromise<JSXOutput> => {
const host = ssr.getLastNode();
const host = ssr.getOrCreateLastNode();
const [componentQrl] = (component as any)[SERIALIZABLE_STATE] as [QRLInternal<OnRenderFn<any>>];
const srcProps = jsx.props;
if (srcProps && srcProps.children) {
Expand Down
Loading
Loading