Skip to content

Commit 53cec56

Browse files
committed
perf(core): yield state processing during resume
1 parent e4da07b commit 53cec56

25 files changed

Lines changed: 707 additions & 223 deletions

.changeset/smart-buckets-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': minor
3+
---
4+
5+
feat: improve client resume responsiveness by splitting state processing into smaller tasks

packages/qwik-router/src/middleware/request-handler/request-event-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,13 @@ const parseRequest = async (
423423
const data = query.get(deps.QDATA_KEY);
424424
if (data) {
425425
try {
426-
return _deserialize(decodeURIComponent(data)) as JSONValue;
426+
return (await _deserialize(decodeURIComponent(data))) as JSONValue;
427427
} catch {
428428
//
429429
}
430430
}
431431
}
432-
return _deserialize(await request.text()) as JSONValue;
432+
return (await _deserialize(await request.text())) as JSONValue;
433433
}
434434
return undefined;
435435
};

packages/qwik-router/src/runtime/src/qwik-router-component.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
402402
historyUpdated = true;
403403
}
404404

405+
actionState.value = undefined;
406+
405407
routeInternal.value = {
406408
type,
407409
dest,
@@ -416,7 +418,6 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
416418
loadRoute(qwikRouterConfig.routes, qwikRouterConfig.cacheModules, dest.pathname);
417419
}
418420

419-
actionState.value = undefined;
420421
routeLocation.isNavigating = true;
421422

422423
return new Promise<void>((resolve) => {

packages/qwik-router/src/runtime/src/server-functions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ export const serverQrl = <T extends ServerFunction>(
493493
})();
494494
} else if (contentType === 'application/qwik-json') {
495495
const str = await res.text();
496-
const obj = _deserialize(str);
496+
const obj = await _deserialize(str);
497497
if (res.status >= 400) {
498498
throw obj;
499499
}
@@ -575,7 +575,7 @@ const deserializeStream = async function* (
575575
const lines = buffer.split(/\n/);
576576
buffer = lines.pop()!;
577577
for (const line of lines) {
578-
const deserializedData = _deserialize(line);
578+
const deserializedData = await _deserialize(line);
579579
yield deserializedData;
580580
}
581581
}

packages/qwik-router/src/runtime/src/use-endpoint.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const loadClientData = async (
5858
}
5959
if ((rsp.headers.get('content-type') || '').includes('json')) {
6060
// we are safe we are reading a q-data.json
61-
return rsp.text().then((text) => {
62-
const clientData = _deserialize<ClientPageData>(text);
61+
return rsp.text().then(async (text) => {
62+
const clientData = await _deserialize<ClientPageData>(text);
6363
if (!clientData) {
6464
// Something went wrong, show to the user
6565
location.href = url.href;

packages/qwik/src/core/client/dom-container.ts

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { assertTrue } from '../shared/error/assert';
66
import { QError, qError } from '../shared/error/error';
77
import { ERROR_CONTEXT, isRecoverable } from '../shared/error/error-handling';
88
import type { QRL } from '../shared/qrl/qrl.public';
9-
import { wrapDeserializerProxy } from '../shared/serdes/deser-proxy';
9+
import { eagerDeserializeStateIterator } from '../shared/serdes/inflate';
1010
import { getObjectById, parseQRL, preprocessState } from '../shared/serdes/index';
11+
import {
12+
createMacroTask,
13+
runYieldingIterator,
14+
scheduleYieldingIterator,
15+
type YieldingIteratorState,
16+
} from '../shared/platform/next-tick';
1117
import { _SharedContainer } from '../shared/shared-container';
1218
import { QContainerValue, type HostElement, type ObjToProxyMap } from '../shared/types';
1319
import { EMPTY_ARRAY } from '../shared/utils/flyweight';
@@ -84,6 +90,90 @@ export const isDomContainer = (container: any): container is DomContainer => {
8490
return container instanceof DomContainer;
8591
};
8692

93+
interface ProcessContainerDataState extends YieldingIteratorState {}
94+
95+
export const processContainerData = (container: IClientContainer): void => {
96+
const domContainer = container as DomContainer;
97+
if (domContainer.$containerDataStarted$ || domContainer.$containerDataReady$) {
98+
return;
99+
}
100+
domContainer.$containerDataStarted$ = true;
101+
processVNodeData(domContainer.document);
102+
onVNodeDataReady(domContainer.document, () => {
103+
if (
104+
!domContainer.$containerDataStarted$ ||
105+
domContainer.$containerDataReady$ ||
106+
domContainer.element.qContainer !== domContainer
107+
) {
108+
return;
109+
}
110+
const state: ProcessContainerDataState = {
111+
$iterator$: domContainer.$processContainerData$(),
112+
$schedule$: undefined!,
113+
$scheduled$: false,
114+
};
115+
state.$schedule$ = createMacroTask(() =>
116+
runYieldingIterator(
117+
state,
118+
() =>
119+
domContainer.$containerDataState$ === state &&
120+
domContainer.element.qContainer === domContainer,
121+
() => markContainerDataReady(domContainer),
122+
() => {
123+
domContainer.$containerDataStarted$ = false;
124+
domContainer.$containerDataState$ = undefined;
125+
}
126+
)
127+
);
128+
domContainer.$containerDataState$ = state;
129+
scheduleYieldingIterator(state);
130+
});
131+
};
132+
133+
export const onContainerDataReady = (container: IClientContainer, callback: () => void): void => {
134+
const domContainer = container as DomContainer;
135+
if (domContainer.$containerDataReady$) {
136+
callback();
137+
} else {
138+
processContainerData(domContainer);
139+
(domContainer.$containerDataCallbacks$ ||= []).push(callback);
140+
}
141+
};
142+
143+
export const whenContainerDataReady = <T>(
144+
container: IClientContainer,
145+
callback: () => T | Promise<T>
146+
): T | Promise<T> => {
147+
const domContainer = container as DomContainer;
148+
if (domContainer.$containerDataReady$) {
149+
return callback();
150+
}
151+
return new Promise<T>((resolve, reject) => {
152+
onContainerDataReady(domContainer, () => {
153+
try {
154+
resolve(callback());
155+
} catch (error) {
156+
reject(error);
157+
}
158+
});
159+
});
160+
};
161+
162+
function markContainerDataReady(container: DomContainer): void {
163+
if (container.$containerDataReady$) {
164+
return;
165+
}
166+
container.$containerDataReady$ = true;
167+
container.$containerDataState$ = undefined;
168+
const callbacks = container.$containerDataCallbacks$;
169+
container.$containerDataCallbacks$ = undefined;
170+
if (callbacks) {
171+
for (let i = 0; i < callbacks.length; i++) {
172+
callbacks[i]();
173+
}
174+
}
175+
}
176+
87177
/** @internal */
88178
export class DomContainer extends _SharedContainer implements IClientContainer {
89179
public element: ContainerElement;
@@ -96,6 +186,10 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
96186
public $instanceHash$: string;
97187
public $forwardRefs$: Array<number> | null = null;
98188
public vNodeLocate: (id: string | Element) => VNode = (id) => vnode_locate(this.rootVNode, id);
189+
public $containerDataStarted$ = false;
190+
public $containerDataReady$ = false;
191+
public $containerDataState$?: ProcessContainerDataState;
192+
public $containerDataCallbacks$?: Array<() => void>;
99193

100194
private $rawStateData$: unknown[];
101195
private $stateData$: unknown[];
@@ -121,10 +215,10 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
121215
this.$setServerData$();
122216
element.qContainer = this;
123217
(element as any).qDestroy = () => this.$destroy$();
124-
onVNodeDataReady(document, () => this.$finalizeResume$());
218+
processContainerData(this);
125219
}
126220

127-
private $finalizeResume$(): void {
221+
*$processContainerData$(): Generator<void, void, void> {
128222
const element = this.element;
129223
if (element.qContainer !== this) {
130224
return;
@@ -134,7 +228,8 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
134228
const lastState = qwikStates[qwikStates.length - 1];
135229
this.$rawStateData$ = JSON.parse(lastState.textContent!);
136230
preprocessState(this.$rawStateData$, this);
137-
this.$stateData$ = wrapDeserializerProxy(this, this.$rawStateData$) as unknown[];
231+
this.$stateData$ = Array(this.$rawStateData$.length / 2);
232+
yield* eagerDeserializeStateIterator(this, this.$rawStateData$, this.$stateData$);
138233
}
139234
this.$hoistStyles$();
140235
element.setAttribute(QContainerAttr, QContainerValue.RESUMED);
@@ -160,6 +255,10 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
160255
document.qVNodeDataReady = false;
161256
document.qVNodeDataState = undefined;
162257
document.qVNodeDataCallbacks = undefined;
258+
this.$containerDataStarted$ = false;
259+
this.$containerDataReady$ = false;
260+
this.$containerDataState$ = undefined;
261+
this.$containerDataCallbacks$ = undefined;
163262
}
164263

165264
/**

packages/qwik/src/core/client/dom-render.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FunctionComponent, JSXOutput } from '../shared/jsx/types/jsx-node';
22
import { isDocument, isElement } from '../shared/utils/element';
33
import { QContainerValue } from '../shared/types';
4-
import { DomContainer, getDomContainer } from './dom-container';
4+
import { DomContainer, getDomContainer, whenContainerDataReady } from './dom-container';
55
import { cleanup } from './vnode-diff';
66
import { QContainerAttr } from '../shared/utils/markers';
77
import type { RenderOptions, RenderResult } from './types';
@@ -11,7 +11,6 @@ import { vnode_setProp } from './vnode-utils';
1111
import { markVNodeDirty } from '../shared/vnode/vnode-dirty';
1212
import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum';
1313
import { NODE_DIFF_DATA_KEY } from '../shared/cursor/cursor-props';
14-
import { whenVNodeDataReady } from './process-vnode-data';
1514

1615
/**
1716
* Render JSX.
@@ -44,7 +43,7 @@ export const render = async (
4443
(parent as Element).setAttribute(QContainerAttr, QContainerValue.RESUMED);
4544

4645
const container = getDomContainer(parent as HTMLElement) as DomContainer;
47-
await whenVNodeDataReady(container.document, () => undefined);
46+
await whenContainerDataReady(container, () => undefined);
4847
container.$serverData$ = opts.serverData || {};
4948
const host = container.rootVNode;
5049
vnode_setProp(host, NODE_DIFF_DATA_KEY, jsxNode);

packages/qwik/src/core/client/process-vnode-data.ts

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
import { VNodeDataChar, VNodeDataSeparator } from '../shared/vnode-data-types';
33
import type { ContainerElement, QDocument } from './types';
44
import type { ElementVNode } from '../shared/vnode/element-vnode';
5-
import { createMacroTask } from '../shared/platform/next-tick';
5+
import {
6+
createMacroTask,
7+
runYieldingIterator,
8+
scheduleYieldingIterator,
9+
type YieldingIteratorState,
10+
} from '../shared/platform/next-tick';
611

7-
const VNODE_DATA_YIELD_INTERVAL = 1000 / 60;
812
const Q_CONTAINER = 'q:container';
913
const Q_CONTAINER_END = '/' + Q_CONTAINER;
1014
const Q_PROPS_SEPARATOR = ':';
@@ -28,11 +32,8 @@ const enum NodeType {
2832
OTHER /* ************************** */ = 0b0000000,
2933
}
3034

31-
interface ProcessVNodeDataState {
35+
interface ProcessVNodeDataState extends YieldingIteratorState {
3236
$document$: QDocument;
33-
$iterator$: Generator<void, void, void>;
34-
$schedule$: () => void;
35-
$scheduled$: boolean;
3637
}
3738

3839
/**
@@ -99,9 +100,19 @@ export function processVNodeData(document: Document): void {
99100
$schedule$: undefined!,
100101
$scheduled$: false,
101102
};
102-
state.$schedule$ = createMacroTask(() => runProcessVNodeData(state));
103+
state.$schedule$ = createMacroTask(() =>
104+
runYieldingIterator(
105+
state,
106+
() => state.$document$.qVNodeDataState === state,
107+
() => markVNodeDataReady(state.$document$),
108+
() => {
109+
state.$document$.qVNodeDataStarted = false;
110+
state.$document$.qVNodeDataState = undefined;
111+
}
112+
)
113+
);
103114
qDocument.qVNodeDataState = state;
104-
scheduleProcessVNodeData(state);
115+
scheduleYieldingIterator(state);
105116
}
106117

107118
export const onVNodeDataReady = (document: Document, callback: () => void): void => {
@@ -132,40 +143,6 @@ export const whenVNodeDataReady = <T>(
132143
});
133144
};
134145

135-
function scheduleProcessVNodeData(state: ProcessVNodeDataState): void {
136-
if (!state.$scheduled$) {
137-
state.$scheduled$ = true;
138-
state.$schedule$();
139-
}
140-
}
141-
142-
function runProcessVNodeData(state: ProcessVNodeDataState): void {
143-
if (state.$document$.qVNodeDataState !== state) {
144-
return;
145-
}
146-
state.$scheduled$ = false;
147-
const deadline = performance.now() + VNODE_DATA_YIELD_INTERVAL;
148-
let count = 0;
149-
try {
150-
while (true) {
151-
const result = state.$iterator$.next();
152-
if (result.done) {
153-
markVNodeDataReady(state.$document$);
154-
return;
155-
}
156-
// Sampling the clock every 32 steps keeps `performance.now()` out of the hottest path.
157-
if ((++count & 31) === 0 && performance.now() >= deadline) {
158-
scheduleProcessVNodeData(state);
159-
return;
160-
}
161-
}
162-
} catch (error) {
163-
state.$document$.qVNodeDataStarted = false;
164-
state.$document$.qVNodeDataState = undefined;
165-
throw error;
166-
}
167-
}
168-
169146
function markVNodeDataReady(document: QDocument): void {
170147
if (document.qVNodeDataReady) {
171148
return;

0 commit comments

Comments
 (0)