Skip to content
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

perf(serdes): vnode tree shaking #6963

Merged
merged 22 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
793d697
perf(ssr): only serialize vnodes that are referenced
wmertens Oct 29, 2024
20c318a
perf(serdes): improve serialization for qrl, PropsProxy, ComputedSignal
wmertens Oct 29, 2024
0dc51ab
perf(wrapProp): minify the sync funcs used
wmertens Oct 29, 2024
afb48cd
fix(tests): make fragments optional during ssr compare
wmertens Oct 29, 2024
943b30e
fix(core): q:container attribute value on resume
wmertens Oct 29, 2024
b87eb11
fix(serdes): array store serialization
wmertens Oct 29, 2024
f93ffb2
fix(vnode): should skip qstyle elements while materializing from DOM
Varixo Oct 29, 2024
c75bcfe
fix(tests): update ssr spec tests WIP
wmertens Oct 29, 2024
c2dd6f3
WIP fix vnode data serialization
wmertens Oct 29, 2024
8b2d7cd
remove ssr render from use-sequential-scope test
Varixo Oct 29, 2024
0ebf9fe
wip
wmertens Oct 29, 2024
5e113ae
perf(ssr): only serialize vnodes that are referenced
wmertens Oct 10, 2024
b45d110
WIP fix vnode data serialization
wmertens Oct 26, 2024
c065386
split spec files, fix vdom-diff
Varixo Nov 1, 2024
8569977
fix rendering util
Varixo Nov 1, 2024
3a66a59
add failing reusing nodes tests
Varixo Nov 2, 2024
c4e1836
serialize all fragments with the q:key attribute
Varixo Nov 8, 2024
2daf040
fix promises inside q:template
Varixo Nov 8, 2024
f72d24d
fix serializing wrapped signal
Varixo Nov 9, 2024
f003095
serialize all vnodes inside interactive component
Varixo Nov 12, 2024
3613ae2
update integration tests
Varixo Nov 13, 2024
c17f585
Merge remote-tracking branch 'origin/build/v2' into lazy-vnodes
wmertens Nov 13, 2024
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
2 changes: 1 addition & 1 deletion packages/docs/src/routes/api/qwik-testing/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
}
],
"kind": "Function",
"content": "```typescript\nexport declare function ssrRenderToDom(jsx: JSXOutput, opts?: {\n debug?: boolean;\n raw?: boolean;\n}): Promise<{\n container: _DomContainer;\n document: Document;\n vNode: _VNode;\n getStyles: () => Record<string, string | string[]>;\n}>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\njsx\n\n\n</td><td>\n\nJSXOutput\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n{ debug?: boolean; raw?: boolean; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nPromise&lt;{ container: \\_DomContainer; document: Document; vNode: \\_VNode; getStyles: () =&gt; Record&lt;string, string \\| string\\[\\]&gt;; }&gt;",
"content": "```typescript\nexport declare function ssrRenderToDom(jsx: JSXOutput, opts?: {\n debug?: boolean;\n raw?: boolean;\n}): Promise<{\n container: _DomContainer;\n document: Document;\n vNode: _VirtualVNode | null;\n getStyles: () => Record<string, string | string[]>;\n}>;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\njsx\n\n\n</td><td>\n\nJSXOutput\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n{ debug?: boolean; raw?: boolean; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nPromise&lt;{ container: \\_DomContainer; document: Document; vNode: \\_VirtualVNode \\| null; getStyles: () =&gt; Record&lt;string, string \\| string\\[\\]&gt;; }&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/testing/rendering.unit-util.tsx",
"mdFile": "core.ssrrendertodom.md"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik-testing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ export declare function ssrRenderToDom(
): Promise<{
container: _DomContainer;
document: Document;
vNode: _VNode;
vNode: _VirtualVNode | null;
getStyles: () => Record<string, string | string[]>;
}>;
```
Expand Down Expand Up @@ -479,7 +479,7 @@ _(Optional)_
</tbody></table>
**Returns:**

Promise&lt;{ container: \_DomContainer; document: Document; vNode: \_VNode; getStyles: () =&gt; Record&lt;string, string \| string[]&gt;; }&gt;
Promise&lt;{ container: \_DomContainer; document: Document; vNode: \_VirtualVNode \| null; getStyles: () =&gt; Record&lt;string, string \| string[]&gt;; }&gt;

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/testing/rendering.unit-util.tsx)

Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ export abstract class _SharedContainer implements Container {
nodeType: number;
id: string;
};
} | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter): SerializationContext;
} | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter, prepVNodeData?: (vNode: any) => void): SerializationContext;
// (undocumented)
abstract setContext<T>(host: HostElement, context: ContextId<T>, value: T): void;
// (undocumented)
Expand Down
5 changes: 4 additions & 1 deletion packages/qwik/src/core/client/dom-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
} from '../shared/utils/scoped-styles';
import { _SharedContainer } from '../shared/shared-container';
import { inflateQRL, parseQRL, wrapDeserializerProxy } from '../shared/shared-serialization';
import { type HostElement, type ObjToProxyMap } from '../shared/types';
import { QContainerValue, type HostElement, type ObjToProxyMap } from '../shared/types';
import { processVNodeData } from './process-vnode-data';
import {
VNodeFlags,
Expand Down Expand Up @@ -100,6 +100,9 @@ export function getDomContainerFromQContainerElement(qContainerElement: Element)
}
}
(container as DomContainer).$serverData$ = { containerAttributes };

qElement.setAttribute(QContainerAttr, QContainerValue.RESUMED);

qElement.qContainer = container;
}
return container;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ describe('processVnodeData', () => {
});
});

const qContainerPaused = { 'q:container': 'paused' };
const qContainerPaused = { 'q:container': 'resumed' };
const qContainerHtml = { 'q:container': 'html' };
function process(html: string): ClientContainer[] {
html = html.trim();
Expand Down
50 changes: 26 additions & 24 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export const vnode_diff = (
*/
const stack: any[] = [];

const asyncQueue: Array<VNode | ValueOrPromise<JSXOutput>> = [];
const asyncQueue: Array<VNode | ValueOrPromise<JSXOutput> | Promise<JSXOutput | JSXChildren>> =
[];

////////////////////////////////
//// Traverse state variables
Expand All @@ -144,9 +145,9 @@ export const vnode_diff = (
let vSiblingsIdx = -1;

/// Current set of JSX children.
let jsxChildren: any[] = null!;
let jsxChildren: JSXChildren[] = null!;
// Current JSX child.
let jsxValue: any = null;
let jsxValue: JSXChildren = null;
let jsxIdx = 0;
let jsxCount = 0;

Expand Down Expand Up @@ -197,7 +198,7 @@ export const vnode_diff = (
expectVirtual(VirtualType.WrappedSignal, null);
descend(
trackSignal(
() => jsxValue.value,
() => (jsxValue as Signal).value,
(vNewNode || vCurrent)!,
EffectProperty.VNODE,
container
Expand Down Expand Up @@ -238,7 +239,7 @@ export const vnode_diff = (
}
}
}
} else if (jsxValue === SkipRender) {
} else if (jsxValue === (SkipRender as JSXChildren)) {
// do nothing, we are skipping this node
journal = [];
} else {
Expand Down Expand Up @@ -327,7 +328,7 @@ export const vnode_diff = (
* In the above example all nodes are on same level so we don't `descendVNode` even thought there
* is an array produced by the `map` function.
*/
function descend(children: any, descendVNode: boolean) {
function descend(children: JSXChildren, descendVNode: boolean) {
if (children == null) {
expectNoChildren();
return;
Expand Down Expand Up @@ -360,7 +361,7 @@ export const vnode_diff = (
advance();
}

function stackPush(children: any, descendVNode: boolean) {
function stackPush(children: JSXChildren, descendVNode: boolean) {
stack.push(jsxChildren, jsxIdx, jsxCount, jsxValue);
if (descendVNode) {
stack.push(vParent, vCurrent, vNewNode, vSiblings, vSiblingsIdx);
Expand Down Expand Up @@ -449,7 +450,8 @@ export const vnode_diff = (
}

function expectProjection() {
const slotName = jsxValue.key as string;
const jsxNode = jsxValue as JSXNode;
const slotName = jsxNode.key as string;
// console.log('expectProjection', JSON.stringify(slotName));
vCurrent = vnode_getProp<VirtualVNode | null>(
vParent, // The parent is the component and it should have our portal.
Expand Down Expand Up @@ -519,14 +521,15 @@ export const vnode_diff = (
}

function getSlotNameKey(vHost: VNode | null) {
const constProps = jsxValue.constProps;
const jsxNode = jsxValue as JSXNode;
const constProps = jsxNode.constProps;
if (constProps && typeof constProps == 'object' && 'name' in constProps) {
const constValue = constProps.name;
if (vHost && constValue instanceof WrappedSignal) {
return trackSignal(() => constValue.value, vHost, EffectProperty.COMPONENT, container);
}
}
return directGetPropsProxyProp(jsxValue, 'name') || QDefaultSlot;
return directGetPropsProxyProp(jsxNode, 'name') || QDefaultSlot;
}

function drainAsyncQueue(): ValueOrPromise<void> {
Expand Down Expand Up @@ -949,11 +952,7 @@ export const vnode_diff = (
}

function expectVirtual(type: VirtualType, jsxKey: string | null) {
if (
vCurrent &&
vnode_isVirtualVNode(vCurrent) &&
vnode_getProp(vCurrent, ELEMENT_KEY, null) === jsxKey
) {
if (vCurrent && vnode_isVirtualVNode(vCurrent) && getKey(vCurrent) === jsxKey) {
// All is good.
return;
} else if (jsxKey !== null) {
Expand Down Expand Up @@ -984,16 +983,17 @@ export const vnode_diff = (
function expectComponent(component: Function) {
const componentMeta = (component as any)[SERIALIZABLE_STATE] as [QRLInternal<OnRenderFn<any>>];
let host = (vNewNode || vCurrent) as VirtualVNode | null;
const jsxNode = jsxValue as JSXNode;
if (componentMeta) {
const jsxProps = jsxValue.props;
const jsxProps = jsxNode.props;
// QComponent
let shouldRender = false;
const [componentQRL] = componentMeta;

const componentHash = componentQRL.$hash$;
const vNodeComponentHash = getComponentHash(host, container.$getObjectById$);

const lookupKey = jsxValue.key || componentHash;
const lookupKey = jsxNode.key || componentHash;
const vNodeLookupKey = getKey(host) || vNodeComponentHash;

const lookupKeysAreEqual = lookupKey === vNodeLookupKey;
Expand Down Expand Up @@ -1036,9 +1036,9 @@ export const vnode_diff = (
container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps);
}
}
jsxValue.children != null && descendContentToProject(jsxValue.children, host);
jsxNode.children != null && descendContentToProject(jsxNode.children, host);
} else {
const lookupKey = jsxValue.key;
const lookupKey = jsxNode.key;
const vNodeLookupKey = getKey(host);
const lookupKeysAreEqual = lookupKey === vNodeLookupKey;

Expand Down Expand Up @@ -1072,7 +1072,7 @@ export const vnode_diff = (
host,
(componentHost || container.rootVNode) as HostElement,
component as OnRenderFn<unknown>,
jsxValue.props
jsxNode.props
);

asyncQueue.push(jsxOutput, host);
Expand All @@ -1094,10 +1094,11 @@ export const vnode_diff = (
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
const jsxNode = jsxValue as JSXNode;
isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.Component);
container.setHostProp(vNewNode, OnRenderProp, componentQRL);
container.setHostProp(vNewNode, ELEMENT_PROPS, jsxProps);
container.setHostProp(vNewNode, ELEMENT_KEY, jsxValue.key);
container.setHostProp(vNewNode, ELEMENT_KEY, jsxNode.key);
}

function insertNewInlineComponent() {
Expand All @@ -1107,10 +1108,11 @@ export const vnode_diff = (
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
const jsxNode = jsxValue as JSXNode;
isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.InlineComponent);
vnode_setProp(vNewNode, ELEMENT_PROPS, jsxValue.props);
if (jsxValue.key) {
vnode_setProp(vNewNode, ELEMENT_KEY, jsxValue.key);
vnode_setProp(vNewNode, ELEMENT_PROPS, jsxNode.props);
if (jsxNode.key) {
vnode_setProp(vNewNode, ELEMENT_KEY, jsxNode.key);
}
}

Expand Down
30 changes: 19 additions & 11 deletions packages/qwik/src/core/client/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export const vnode_ensureElementInflated = (vnode: VNode) => {
for (let idx = 0; idx < attributes.length; idx++) {
const attr = attributes[idx];
const key = attr.name;
if (key == Q_PROPS_SEPARATOR || !key) {
if (key === Q_PROPS_SEPARATOR || !key) {
// SVG in Domino does not support ':' so it becomes an empty string.
// all attributes after the ':' are considered immutable, and so we ignore them.
break;
Expand Down Expand Up @@ -1387,13 +1387,17 @@ const isQStyleElement = (node: Node | null): node is Element => {

const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => {
let vFirstChild: VNode | null = null;

const skipStyleElements = () => {
while (isQStyleElement(child)) {
// skip over style elements, as those need to be moved to the head.
// VNode pretends that `<style q:style q:sstyle>` elements do not exist.
child = fastNextSibling(child);
}
};
// materialize from DOM
let child = firstChild;
while (isQStyleElement(child)) {
// skip over style elements, as those need to be moved to the head.
// VNode pretends that `<style q:style q:sstyle>` elements do not exist.
child = fastNextSibling(child);
}
skipStyleElements();
let vChild: VNode | null = null;
while (child) {
const nodeType = fastNodeType(child);
Expand All @@ -1413,6 +1417,7 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => {
vParent[ElementVNodeProps.firstChild] = vFirstChild = vChild;
}
child = fastNextSibling(child);
skipStyleElements();
}
vParent[ElementVNodeProps.lastChild] = vChild || null;
vParent[ElementVNodeProps.firstChild] = vFirstChild;
Expand All @@ -1434,7 +1439,7 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[]
const keys: string[] = [];
for (let i = vnode_getPropStartIndex(vnode); i < vnode.length; i = i + 2) {
const key = vnode[i] as string;
if (!key.startsWith(':')) {
if (!key.startsWith(Q_PROPS_SEPARATOR)) {
keys.push(key);
}
}
Expand Down Expand Up @@ -1556,7 +1561,8 @@ export function vnode_toString(
this: VNode | null,
depth: number = 10,
offset: string = '',
materialize: boolean = false
materialize: boolean = false,
siblings = false
): string {
let vnode = this;
if (depth === 0) {
Expand Down Expand Up @@ -1586,7 +1592,8 @@ export function vnode_toString(
VirtualTypeName[VirtualType.Virtual];
strings.push('<' + name + attrs.join('') + '>');
const child = vnode_getFirstChild(vnode);
child && strings.push(' ' + vnode_toString.call(child, depth - 1, offset + ' ', true));
child &&
strings.push(' ' + vnode_toString.call(child, depth - 1, offset + ' ', true, true));
strings.push('</' + name + '>');
} else if (vnode_isElementVNode(vnode)) {
const tag = vnode_getElementName(vnode);
Expand All @@ -1613,13 +1620,14 @@ export function vnode_toString(
strings.push('<' + tag + attrs.join('') + '>');
if (vnode_isMaterialized(vnode) || materialize) {
const child = vnode_getFirstChild(vnode);
child && strings.push(' ' + vnode_toString.call(child, depth - 1, offset + ' ', true));
child &&
strings.push(' ' + vnode_toString.call(child, depth - 1, offset + ' ', true, true));
} else {
strings.push(' <!-- not materialized --!>');
}
strings.push('</' + tag + '>');
}
vnode = vnode_getNextSibling(vnode) || null;
vnode = (siblings && vnode_getNextSibling(vnode)) || null;
} while (vnode);
return strings.join('\n' + offset);
}
Expand Down
8 changes: 5 additions & 3 deletions packages/qwik/src/core/shared/shared-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export abstract class _SharedContainer implements Container {
this.$locale$ = locale;
this.$version$ = version;
this.$storeProxyMap$ = new WeakMap();
this.$getObjectById$ = (id: number | string) => {
this.$getObjectById$ = (_id: number | string) => {
throw Error('Not implemented');
};

Expand All @@ -50,15 +50,17 @@ export abstract class _SharedContainer implements Container {
new (...rest: any[]): { nodeType: number; id: string };
} | null,
symbolToChunkResolver: SymbolToChunkResolver,
writer?: StreamWriter
writer?: StreamWriter,
prepVNodeData?: (vNode: any) => void
): SerializationContext {
return createSerializationContext(
NodeConstructor,
symbolToChunkResolver,
this.getHostProp.bind(this),
this.setHostProp.bind(this),
this.$storeProxyMap$,
writer
writer,
prepVNodeData
);
}

Expand Down
Loading