Skip to content

Commit a7e0402

Browse files
authored
Merge pull request #8055 from QwikDev/v2-ssr-perf
perf(ssr): SSR performance improvements
2 parents 9d0ba09 + da7264b commit a7e0402

File tree

6 files changed

+441
-10
lines changed

6 files changed

+441
-10
lines changed

packages/qwik/src/core/client/vnode.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,15 +690,22 @@ export const vnode_locate = (rootVNode: ElementVNode, id: string | Element): VNo
690690
// We need to find the vnode.
691691
let parent = refElement;
692692
const elementPath: Element[] = [refElement];
693-
while (parent && parent !== containerElement) {
693+
while (parent && parent !== containerElement && !(parent as QElement).vNode) {
694694
parent = parent.parentElement!;
695695
elementPath.push(parent);
696696
}
697+
if ((parent as QElement).vNode) {
698+
vNode = (parent as QElement).vNode as ElementVNode;
699+
}
697700
// Start at rootVNode and follow the `elementPath` to find the vnode.
698701
for (let i = elementPath.length - 2; i >= 0; i--) {
699702
vNode = vnode_getVNodeForChildNode(vNode as ElementVNode, elementPath[i]);
700703
}
701-
elementOffset != -1 && qVNodeRefs!.set(elementOffset, vNode as ElementVNode);
704+
705+
if (elementOffset != -1) {
706+
(refElement as QElement).vNode = vNode;
707+
qVNodeRefs!.set(elementOffset, vNode as ElementVNode);
708+
}
702709
} else {
703710
vNode = refElement;
704711
}
@@ -1261,8 +1268,7 @@ export const vnode_getElementName = (vnode: ElementVNode): string => {
12611268
return elementName;
12621269
};
12631270

1264-
export const vnode_getText = (vnode: TextVNode): string => {
1265-
const textVNode = ensureTextVNode(vnode);
1271+
export const vnode_getText = (textVNode: TextVNode): string => {
12661272
let text = textVNode.text;
12671273
if (text === undefined) {
12681274
text = textVNode.text = textVNode.textNode!.nodeValue!;

packages/qwik/src/core/shared/serdes/inflate.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { getStoreHandler } from '../../reactive-primitives/impl/store';
77
import type { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl';
88
import type { SubscriptionData } from '../../reactive-primitives/subscription-data';
99
import {
10+
EffectProperty,
1011
NEEDS_COMPUTATION,
1112
SignalFlags,
1213
type AllSignalFlags,
1314
type AsyncComputeQRL,
14-
type EffectProperty,
1515
type EffectSubscription,
1616
type StoreFlags,
1717
} from '../../reactive-primitives/types';
@@ -30,6 +30,14 @@ import { allocate, pendingStoreTargets } from './allocate';
3030
import { needsInflation } from './deser-proxy';
3131
import { resolvers } from './allocate';
3232
import { TypeIds } from './constants';
33+
import {
34+
vnode_getFirstChild,
35+
vnode_getText,
36+
vnode_isTextVNode,
37+
vnode_isVNode,
38+
} from '../../client/vnode';
39+
import type { VirtualVNode } from '../../client/vnode-impl';
40+
import { isString } from '../utils/types';
3341

3442
export const inflate = (
3543
container: DeserializeContainer,
@@ -64,7 +72,7 @@ export const inflate = (
6472
case TypeIds.Task:
6573
const task = target as Task;
6674
const v = data as any[];
67-
task.$qrl$ = _inflateQRL(container, v[0]);
75+
task.$qrl$ = v[0];
6876
task.$flags$ = v[1];
6977
task.$index$ = v[2];
7078
task.$el$ = v[3] as HostElement;
@@ -131,6 +139,8 @@ export const inflate = (
131139
signal.$flags$ |= SignalFlags.INVALID;
132140
signal.$hostElement$ = d[4];
133141
signal.$effects$ = new Set(d.slice(5) as EffectSubscription[]);
142+
143+
inflateWrappedSignalValue(signal);
134144
break;
135145
}
136146
case TypeIds.AsyncComputedSignal: {
@@ -283,8 +293,14 @@ export const _eagerDeserializeArray = (
283293
return output;
284294
};
285295
export function _inflateQRL(container: DeserializeContainer, qrl: QRLInternal<any>) {
296+
if (qrl.$captureRef$) {
297+
// early return if capture references are already set and qrl is already inflated
298+
return qrl;
299+
}
286300
const captureIds = qrl.$capture$;
287301
qrl.$captureRef$ = captureIds ? captureIds.map((id) => container.$getObjectById$(id)) : null;
302+
// clear serialized capture references
303+
qrl.$capture$ = null;
288304
if (container.element) {
289305
qrl.$setContainer$(container.element);
290306
}
@@ -300,3 +316,36 @@ export function deserializeData(container: DeserializeContainer, typeId: number,
300316
}
301317
return propValue;
302318
}
319+
export function inflateWrappedSignalValue(signal: WrappedSignalImpl<unknown>) {
320+
if (signal.$hostElement$ !== null && vnode_isVNode(signal.$hostElement$)) {
321+
const hostVNode = signal.$hostElement$ as VirtualVNode;
322+
const effects = signal.$effects$;
323+
let hasAttrValue = false;
324+
if (effects) {
325+
// Find string keys (attribute names) in the effect back refs
326+
for (const [_, key] of effects) {
327+
if (isString(key)) {
328+
// This is an attribute name, try to read its value
329+
const attrValue = hostVNode.getAttr(key);
330+
if (attrValue !== null) {
331+
signal.$untrackedValue$ = attrValue;
332+
hasAttrValue = true;
333+
break; // Take first non-null attribute value
334+
}
335+
}
336+
}
337+
}
338+
339+
if (!hasAttrValue) {
340+
// If no attribute value found, check if this is a text content signal
341+
const firstChild = vnode_getFirstChild(hostVNode);
342+
if (
343+
firstChild &&
344+
hostVNode.firstChild === hostVNode.lastChild &&
345+
vnode_isTextVNode(firstChild)
346+
) {
347+
signal.$untrackedValue$ = vnode_getText(firstChild);
348+
}
349+
}
350+
}
351+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { NEEDS_COMPUTATION, EffectProperty } from '../../reactive-primitives/types';
3+
import { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl';
4+
import { VNodeFlags } from '../../client/types';
5+
import { ElementVNode, VirtualVNode } from '../../client/vnode-impl';
6+
import { inflateWrappedSignalValue } from './inflate';
7+
8+
describe('inflateWrappedSignalValue', () => {
9+
it('should read value from class attribute', () => {
10+
const signal = new WrappedSignalImpl(
11+
null,
12+
(val: boolean) => (val ? 'active' : 'inactive'),
13+
[true],
14+
null
15+
);
16+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
17+
18+
const element = {
19+
attributes: [{ name: 'class', value: 'active' }],
20+
} as any;
21+
22+
const vnode = new ElementVNode(
23+
VNodeFlags.Element,
24+
null, // parent
25+
null, // previousSibling
26+
null, // nextSibling
27+
null, // firstChild
28+
null, // lastChild
29+
element,
30+
'div'
31+
);
32+
vnode.setAttr('class', 'active', null);
33+
34+
signal.$hostElement$ = vnode;
35+
36+
signal.$effects$ = new Set([
37+
[vnode, 'class', null, null], // EffectSubscription: [consumer, property, backRefs, data]
38+
] as any);
39+
40+
inflateWrappedSignalValue(signal);
41+
42+
expect(signal.$untrackedValue$).toBe('active');
43+
expect(signal.$untrackedValue$).not.toBe(NEEDS_COMPUTATION);
44+
});
45+
46+
it('should read value from data-* attribute', () => {
47+
const signal = new WrappedSignalImpl(null, (val: string) => val, ['initial'], null);
48+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
49+
50+
const element = {
51+
attributes: [{ name: 'data-state', value: 'initial' }],
52+
} as any;
53+
54+
const vnode = new ElementVNode(
55+
VNodeFlags.Element,
56+
null,
57+
null,
58+
null,
59+
null,
60+
null,
61+
element,
62+
'div'
63+
);
64+
vnode.setAttr('data-state', 'initial', null);
65+
66+
signal.$hostElement$ = vnode;
67+
signal.$effects$ = new Set([[vnode, 'data-state', null, null]] as any);
68+
69+
inflateWrappedSignalValue(signal);
70+
71+
expect(signal.$untrackedValue$).toBe('initial');
72+
});
73+
74+
it('should skip non-string effect keys (EffectProperty enum)', () => {
75+
const signal = new WrappedSignalImpl(null, () => 'computed', [], null);
76+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
77+
78+
const element = {
79+
attributes: [],
80+
} as any;
81+
82+
const vnode = new ElementVNode(
83+
VNodeFlags.Element,
84+
null,
85+
null,
86+
null,
87+
null,
88+
null,
89+
element,
90+
'div'
91+
);
92+
93+
signal.$hostElement$ = vnode;
94+
signal.$effects$ = new Set([[vnode, EffectProperty.VNODE, null, null]] as any);
95+
96+
inflateWrappedSignalValue(signal);
97+
98+
expect(signal.$untrackedValue$).toBe(NEEDS_COMPUTATION);
99+
});
100+
101+
it('should not set value when attribute is null', () => {
102+
const signal = new WrappedSignalImpl(null, () => 'computed', [], null);
103+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
104+
105+
const element = {
106+
attributes: [],
107+
} as any;
108+
109+
const vnode = new ElementVNode(
110+
VNodeFlags.Element,
111+
null,
112+
null,
113+
null,
114+
null,
115+
null,
116+
element,
117+
'div'
118+
);
119+
120+
signal.$hostElement$ = vnode;
121+
signal.$effects$ = new Set([[vnode, 'missing-attr', null, null]] as any);
122+
123+
inflateWrappedSignalValue(signal);
124+
125+
expect(signal.$untrackedValue$).toBe(NEEDS_COMPUTATION);
126+
});
127+
128+
it('should take first non-null attribute when multiple effects exist', () => {
129+
const signal = new WrappedSignalImpl(null, () => 'computed', [], null);
130+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
131+
132+
const element = {
133+
attributes: [
134+
{ name: 'first-attr', value: 'first-value' },
135+
{ name: 'second-attr', value: 'second-value' },
136+
],
137+
} as unknown as HTMLElement;
138+
139+
const vnode = new ElementVNode(
140+
VNodeFlags.Element,
141+
null,
142+
null,
143+
null,
144+
null,
145+
null,
146+
element,
147+
'div'
148+
);
149+
vnode.setAttr('first-attr', 'first-value', null);
150+
vnode.setAttr('second-attr', 'second-value', null);
151+
152+
signal.$hostElement$ = vnode;
153+
signal.$effects$ = new Set([
154+
[vnode, 'first-attr', null, null],
155+
[vnode, 'second-attr', null, null],
156+
]);
157+
158+
inflateWrappedSignalValue(signal);
159+
160+
expect(signal.$untrackedValue$).toBe('first-value');
161+
});
162+
163+
it('should read from text node when used in text content (not attributes)', () => {
164+
const signal = new WrappedSignalImpl(null, () => 'hello', [], null);
165+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
166+
167+
const textNode = { nodeValue: 'hello' } as any;
168+
const textVNode = { flags: VNodeFlags.Text, textNode } as any;
169+
170+
const vnode = new VirtualVNode(VNodeFlags.Virtual, null, null, null, textVNode, textVNode);
171+
172+
signal.$hostElement$ = vnode;
173+
// No attribute effects, only VNODE effect
174+
signal.$effects$ = new Set([[vnode, EffectProperty.VNODE, null, null]] as any);
175+
176+
inflateWrappedSignalValue(signal);
177+
178+
expect(signal.$untrackedValue$).toBe('hello');
179+
});
180+
181+
it('should prefer attribute over text node when both exist', () => {
182+
const signal = new WrappedSignalImpl(null, () => 'value', [], null);
183+
signal.$untrackedValue$ = NEEDS_COMPUTATION;
184+
185+
// Element with both attribute and text child
186+
const textNode = { nodeValue: 'text-value' } as any;
187+
const textVNode = { flags: VNodeFlags.Text, textNode } as any;
188+
const element = {
189+
attributes: [{ name: 'data-value', value: 'attr-value' }],
190+
childNodes: [textNode],
191+
} as any;
192+
193+
const vnode = new ElementVNode(
194+
VNodeFlags.Element,
195+
null,
196+
null,
197+
null,
198+
textVNode,
199+
textVNode,
200+
element,
201+
'div'
202+
);
203+
vnode.setAttr('data-value', 'attr-value', null);
204+
205+
signal.$hostElement$ = vnode;
206+
signal.$effects$ = new Set([[vnode, 'data-value', null, null]] as any);
207+
208+
inflateWrappedSignalValue(signal);
209+
210+
// Should prefer attribute
211+
expect(signal.$untrackedValue$).toBe('attr-value');
212+
});
213+
});

packages/qwik/src/server/ssr-node.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ export class SsrNode implements ISsrNode {
121121
}
122122

123123
setTreeNonUpdatable(): void {
124-
this.flags &= ~SsrNodeFlags.Updatable;
125-
if (this.children) {
126-
for (const child of this.children) {
127-
(child as SsrNode).setTreeNonUpdatable();
124+
if (this.flags & SsrNodeFlags.Updatable) {
125+
this.flags &= ~SsrNodeFlags.Updatable;
126+
if (this.children) {
127+
for (const child of this.children) {
128+
(child as SsrNode).setTreeNonUpdatable();
129+
}
128130
}
129131
}
130132
}

0 commit comments

Comments
 (0)