Skip to content
Open
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/eleven-worms-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': minor
---

feat: improve client resume responsiveness by splitting startup DOM processing into smaller tasks
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 @@ -223,7 +223,7 @@
}
],
"kind": "Function",
"content": "```typescript\nexport declare function vnode_fromJSX(jsx: JSXOutput): {\n vParent: _VirtualVNode | _ElementVNode;\n vNode: _VNode | null;\n document: _QDocument;\n container: ClientContainer;\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</tbody></table>\n\n**Returns:**\n\n{ vParent: \\_VirtualVNode \\| \\_ElementVNode; vNode: \\_VNode \\| null; document: \\_QDocument; container: ClientContainer; }",
"content": "```typescript\nexport declare function vnode_fromJSX(jsx: JSXOutput): {\n vParent: _ElementVNode | _VirtualVNode;\n vNode: _VNode | null;\n document: _QDocument;\n container: ClientContainer;\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</tbody></table>\n\n**Returns:**\n\n{ vParent: \\_ElementVNode \\| \\_VirtualVNode; vNode: \\_VNode \\| null; document: \\_QDocument; container: ClientContainer; }",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/testing/vdom-diff.unit-util.ts",
"mdFile": "core.vnode_fromjsx.md"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik-testing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ Promise&lt;Event \| null&gt;

```typescript
export declare function vnode_fromJSX(jsx: JSXOutput): {
vParent: _VirtualVNode | _ElementVNode;
vParent: _ElementVNode | _VirtualVNode;
vNode: _VNode | null;
document: _QDocument;
container: ClientContainer;
Expand Down Expand Up @@ -649,7 +649,7 @@ JSXOutput

**Returns:**

\{ vParent: \_VirtualVNode \| \_ElementVNode; vNode: \_VNode \| null; document: \_QDocument; container: ClientContainer; }
\{ vParent: \_ElementVNode \| \_VirtualVNode; vNode: \_VNode \| null; document: \_QDocument; container: ClientContainer; }

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

Expand Down
23 changes: 17 additions & 6 deletions packages/qwik/src/core/client/dom-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import type { ElementVNode } from '../shared/vnode/element-vnode';
import type { VirtualVNode } from '../shared/vnode/virtual-vnode';
import type { VNode } from '../shared/vnode/vnode';
import type { ContextId } from '../use/use-context';
import { processVNodeData } from './process-vnode-data';
import { onVNodeDataReady, processVNodeData } from './process-vnode-data';
import {
VNodeFlags,
type ContainerElement,
Expand Down Expand Up @@ -116,14 +116,19 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
this.$rawStateData$ = [];
this.$stateData$ = [];
const document = this.element.ownerDocument as QDocument;
if (!document.qVNodeData) {
processVNodeData(document);
}
processVNodeData(document);
this.$qFuncs$ = getQFuncs(document, this.$instanceHash$) || EMPTY_ARRAY;
this.$setServerData$();
element.setAttribute(QContainerAttr, QContainerValue.RESUMED);
element.qContainer = this;
(element as any).qDestroy = () => this.$destroy$();
onVNodeDataReady(document, () => this.$finalizeResume$());
}

private $finalizeResume$(): void {
const element = this.element;
if (element.qContainer !== this) {
return;
}
const qwikStates = element.querySelectorAll('script[type="qwik/state"]');
if (qwikStates.length !== 0) {
const lastState = qwikStates[qwikStates.length - 1];
Expand All @@ -132,6 +137,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
this.$stateData$ = wrapDeserializerProxy(this, this.$rawStateData$) as unknown[];
}
this.$hoistStyles$();
element.setAttribute(QContainerAttr, QContainerValue.RESUMED);
if (!qTest && element.isConnected) {
element.dispatchEvent(new CustomEvent('qresume', { bubbles: true }));
}
Expand All @@ -148,7 +154,12 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
el.qVnodeData = undefined;
el.qVNodeRefs = undefined;
el.removeAttribute(QContainerAttr);
(el.ownerDocument as QDocument).qVNodeData = undefined!;
const document = el.ownerDocument as QDocument;
document.qVNodeData = undefined!;
document.qVNodeDataStarted = false;
document.qVNodeDataReady = false;
document.qVNodeDataState = undefined;
document.qVNodeDataCallbacks = undefined;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/qwik/src/core/client/dom-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { vnode_setProp } from './vnode-utils';
import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props';
import { whenVNodeDataReady } from './process-vnode-data';

/**
* Render JSX.
Expand Down Expand Up @@ -43,6 +44,7 @@ export const render = async (
(parent as Element).setAttribute(QContainerAttr, QContainerValue.RESUMED);

const container = getDomContainer(parent as HTMLElement) as DomContainer;
await whenVNodeDataReady(container.document, () => undefined);
container.$serverData$ = opts.serverData || {};
const host = container.rootVNode;
vnode_setProp(host, NODE_DIFF_DATA_KEY, jsxNode);
Expand Down
184 changes: 150 additions & 34 deletions packages/qwik/src/core/client/process-vnode-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@
import { VNodeDataChar, VNodeDataSeparator } from '../shared/vnode-data-types';
import type { ContainerElement, QDocument } from './types';
import type { ElementVNode } from '../shared/vnode/element-vnode';
import { createMacroTask } from '../shared/platform/next-tick';

const VNODE_DATA_YIELD_INTERVAL = 1000 / 60;
const Q_CONTAINER = 'q:container';
const Q_CONTAINER_END = '/' + Q_CONTAINER;
const Q_PROPS_SEPARATOR = ':';
const Q_SHADOW_ROOT = 'q:shadowroot';
const Q_IGNORE = 'q:ignore';
const Q_IGNORE_END = '/' + Q_IGNORE;
const Q_CONTAINER_ISLAND = 'q:container-island';
const Q_CONTAINER_ISLAND_END = '/' + Q_CONTAINER_ISLAND;

const enum NodeType {
CONTAINER_MASK /* ***************** */ = 0b0000001,
ELEMENT /* ************************ */ = 0b0000010, // regular element
ELEMENT_CONTAINER /* ************** */ = 0b0000011, // container element need to descend into it
ELEMENT_SHADOW_ROOT_WRAPPER /* **** */ = 0b0000110, // shadow root wrapper element with q:shadowroot attribute
COMMENT_SKIP_START /* ************* */ = 0b0001001, // Comment but skip the content until COMMENT_SKIP_END
COMMENT_SKIP_END /* *************** */ = 0b0001000, // Comment end
COMMENT_IGNORE_START /* *********** */ = 0b0010000, // Comment ignore, descend into children and skip the content until COMMENT_ISLAND_START
COMMENT_IGNORE_END /* ************* */ = 0b0100000, // Comment ignore end
COMMENT_ISLAND_START /* *********** */ = 0b1000001, // Comment island, count elements for parent container until COMMENT_ISLAND_END
COMMENT_ISLAND_END /* ************* */ = 0b1000000, // Comment island end
OTHER /* ************************** */ = 0b0000000,
}

interface ProcessVNodeDataState {
$document$: QDocument;
$iterator$: Generator<void, void, void>;
$schedule$: () => void;
$scheduled$: boolean;
}

/**
* Process the VNodeData script tags and store the VNodeData in the VNodeDataMap.
Expand Down Expand Up @@ -54,15 +86,102 @@ import type { ElementVNode } from '../shared/vnode/element-vnode';
* - Attach all `qwik/vnode` scripts (not the data contain within them) to the `q:container` element.
* - Walk the tree and process each `q:container` element.
*/
export function processVNodeData(document: Document) {
const Q_CONTAINER = 'q:container';
const Q_CONTAINER_END = '/' + Q_CONTAINER;
const Q_PROPS_SEPARATOR = ':';
const Q_SHADOW_ROOT = 'q:shadowroot';
const Q_IGNORE = 'q:ignore';
const Q_IGNORE_END = '/' + Q_IGNORE;
const Q_CONTAINER_ISLAND = 'q:container-island';
const Q_CONTAINER_ISLAND_END = '/' + Q_CONTAINER_ISLAND;
export function processVNodeData(document: Document): void {
const qDocument = document as QDocument;
if (qDocument.qVNodeDataStarted || qDocument.qVNodeDataReady) {
return;
}
qDocument.qVNodeDataStarted = true;
qDocument.qVNodeData || (qDocument.qVNodeData = new WeakMap<Element, string>());
const state: ProcessVNodeDataState = {
$document$: qDocument,
$iterator$: processVNodeDataIterator(document),
$schedule$: undefined!,
$scheduled$: false,
};
state.$schedule$ = createMacroTask(() => runProcessVNodeData(state));
qDocument.qVNodeDataState = state;
scheduleProcessVNodeData(state);
}

export const onVNodeDataReady = (document: Document, callback: () => void): void => {
const qDocument = document as QDocument;
if (qDocument.qVNodeDataReady) {
callback();
} else {
(qDocument.qVNodeDataCallbacks ||= []).push(callback);
}
};

export const whenVNodeDataReady = <T>(
document: Document,
callback: () => T | Promise<T>
): T | Promise<T> => {
const qDocument = document as QDocument;
if (qDocument.qVNodeDataReady) {
return callback();
}
return new Promise<T>((resolve, reject) => {
onVNodeDataReady(document, () => {
try {
resolve(callback());
} catch (error) {
reject(error);
}
});
});
};

function scheduleProcessVNodeData(state: ProcessVNodeDataState): void {
if (!state.$scheduled$) {
state.$scheduled$ = true;
state.$schedule$();
}
}

function runProcessVNodeData(state: ProcessVNodeDataState): void {
if (state.$document$.qVNodeDataState !== state) {
return;
}
state.$scheduled$ = false;
const deadline = performance.now() + VNODE_DATA_YIELD_INTERVAL;
let count = 0;
try {
while (true) {
const result = state.$iterator$.next();
if (result.done) {
markVNodeDataReady(state.$document$);
return;
}
// Sampling the clock every 32 steps keeps `performance.now()` out of the hottest path.
if ((++count & 31) === 0 && performance.now() >= deadline) {
scheduleProcessVNodeData(state);
return;
}
}
} catch (error) {
state.$document$.qVNodeDataStarted = false;
state.$document$.qVNodeDataState = undefined;
throw error;
}
}

function markVNodeDataReady(document: QDocument): void {
if (document.qVNodeDataReady) {
return;
}
document.qVNodeDataReady = true;
document.qVNodeDataState = undefined;
const callbacks = document.qVNodeDataCallbacks;
document.qVNodeDataCallbacks = undefined;
if (callbacks) {
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
}

function* processVNodeDataIterator(document: Document): Generator<void, void, void> {
const qDocument = document as QDocument;
const vNodeDataMap =
qDocument.qVNodeData || (qDocument.qVNodeData = new WeakMap<Element, string>());
Expand All @@ -84,41 +203,33 @@ export function processVNodeData(document: Document) {
const getNodeType = getter(prototype, 'nodeType') as (this: Node) => number;

// Process all of the `qwik/vnode` script tags by attaching them to the corresponding containers.
const attachVnodeDataAndRefs = (element: Document | ShadowRoot) => {
const attachVnodeDataAndRefs = function* (
element: Document | ShadowRoot
): Generator<void, void, void> {
const scripts = element.querySelectorAll('script[type="qwik/vnode"]');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
const qContainerElement = script.closest('[q\\:container]') as ContainerElement | null;
qContainerElement!.qVnodeData = script.textContent!;
qContainerElement!.qVNodeRefs = new Map<number, Element | ElementVNode>();
yield;
}
const shadowRoots = element.querySelectorAll('[q\\:shadowroot]');
for (let i = 0; i < shadowRoots.length; i++) {
const parent = shadowRoots[i];
const shadowRoot = parent.shadowRoot;
shadowRoot && attachVnodeDataAndRefs(shadowRoot);
if (shadowRoot) {
yield* attachVnodeDataAndRefs(shadowRoot);
}
yield;
}
};
attachVnodeDataAndRefs(document);
yield* attachVnodeDataAndRefs(document);

///////////////////////////////
// Functions to consume the tree.
///////////////////////////////

const enum NodeType {
CONTAINER_MASK /* ***************** */ = 0b0000001,
ELEMENT /* ************************ */ = 0b0000010, // regular element
ELEMENT_CONTAINER /* ************** */ = 0b0000011, // container element need to descend into it
ELEMENT_SHADOW_ROOT_WRAPPER /* **** */ = 0b0000110, // shadow root wrapper element with q:shadowroot attribute
COMMENT_SKIP_START /* ************* */ = 0b0001001, // Comment but skip the content until COMMENT_SKIP_END
COMMENT_SKIP_END /* *************** */ = 0b0001000, // Comment end
COMMENT_IGNORE_START /* *********** */ = 0b0010000, // Comment ignore, descend into children and skip the content until COMMENT_ISLAND_START
COMMENT_IGNORE_END /* ************* */ = 0b0100000, // Comment ignore end
COMMENT_ISLAND_START /* *********** */ = 0b1000001, // Comment island, count elements for parent container until COMMENT_ISLAND_END
COMMENT_ISLAND_END /* ************* */ = 0b1000000, // Comment island end
OTHER /* ************************** */ = 0b0000000,
}

/**
* Looks up which type of node this is in a monomorphic way which should be faster.
*
Expand Down Expand Up @@ -170,15 +281,15 @@ export function processVNodeData(document: Document) {
* @param exitNode The node which represents the last node and we should exit.
* @param qVNodeRefs Place to store the VNodeRefs
*/
const walkContainer = (
const walkContainer = function* (
walker: TreeWalker,
containerNode: Node | null,
node: Node | null,
exitNode: Node | null,
vData: string,
qVNodeRefs: Map<number, Element | ElementVNode>,
prefix: string
) => {
): Generator<void, void, void> {
const vData_length = vData.length;
/// Stores the current element index as the TreeWalker traverses the DOM.
let elementIdx = 0;
Expand Down Expand Up @@ -206,6 +317,10 @@ export function processVNodeData(document: Document) {
return elementsToSkip;
};

if (!node) {
return;
}

do {
if (node === exitNode) {
return;
Expand All @@ -215,12 +330,12 @@ export function processVNodeData(document: Document) {
if (nodeType === NodeType.ELEMENT_CONTAINER) {
// If we are in a container, we need to skip the children.
const container = node as ContainerElement;
let cursor = node;
let cursor: Node | null = node;
while (cursor && !(nextNode = nextSibling(cursor))) {
cursor = cursor!.parentNode;
}
// console.log('EXIT', nextNode?.outerHTML);
walkContainer(
yield* walkContainer(
walker,
container,
node,
Expand All @@ -230,7 +345,7 @@ export function processVNodeData(document: Document) {
prefix + ' '
);
} else if (nodeType === NodeType.COMMENT_IGNORE_START) {
let islandNode = node;
let islandNode: Node | null = node;
do {
islandNode = walker.nextNode();
if (!islandNode) {
Expand Down Expand Up @@ -264,14 +379,14 @@ export function processVNodeData(document: Document) {
}
} while (getFastNodeType(nextNode) !== NodeType.COMMENT_SKIP_END);
// console.log('EXIT', nextNode?.outerHTML);
walkContainer(walker, node, node, nextNode, '', null!, prefix + ' ');
yield* walkContainer(walker, node, node, nextNode, '', null!, prefix + ' ');
} else if (nodeType === NodeType.ELEMENT_SHADOW_ROOT_WRAPPER) {
// If we are in a shadow root, we need to get the shadow root element.
nextNode = nextSibling(node);
const shadowRootContainer = node as Element | null;
const shadowRoot = shadowRootContainer?.shadowRoot;
if (shadowRoot) {
walkContainer(
yield* walkContainer(
// we need to create a new walker for the shadow root
document.createTreeWalker(
shadowRoot,
Expand Down Expand Up @@ -331,6 +446,7 @@ export function processVNodeData(document: Document) {
}
elementIdx++;
}
yield;
} while ((node = nextNode || walker.nextNode()));
};

Expand All @@ -340,7 +456,7 @@ export function processVNodeData(document: Document) {
0x1 /* NodeFilter.SHOW_ELEMENT */ | 0x80 /* NodeFilter.SHOW_COMMENT */
);

walkContainer(walker, null, walker.firstChild(), null, '', null!, '');
yield* walkContainer(walker, null, walker.firstChild(), null, '', null!, '');
}

const isSeparator = (ch: number) =>
Expand Down
Loading
Loading