Skip to content

Commit 141d9e8

Browse files
committed
feat(core): inline qwikloader embeds at 30kB SSR
1 parent 85af918 commit 141d9e8

File tree

3 files changed

+93
-65
lines changed

3 files changed

+93
-65
lines changed

packages/qwik/src/core/tests/container.spec.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { walkJSX } from '@qwik.dev/core/testing';
22
import crypto from 'node:crypto';
3-
import { describe, expect, it } from 'vitest';
3+
import { describe, expect, it, vi } from 'vitest';
44
import { ssrCreateContainer } from '../../server/ssr-container';
55
import { SsrNode } from '../../server/ssr-node';
66
import { createDocument } from '../../testing/document';
@@ -22,6 +22,11 @@ import { _qrlSync } from '../shared/qrl/qrl.public';
2222
import { SignalFlags } from '../reactive-primitives/types';
2323
import type { VNode } from '../client/vnode-impl';
2424

25+
vi.hoisted(() => {
26+
vi.stubGlobal('QWIK_LOADER_DEFAULT_MINIFIED', 'min');
27+
vi.stubGlobal('QWIK_LOADER_DEFAULT_DEBUG', 'debug');
28+
});
29+
2530
describe('serializer v2', () => {
2631
describe('rendering', () => {
2732
it('should do basic serialize/deserialize', async () => {

packages/qwik/src/core/tests/render-api.spec.tsx

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -383,59 +383,68 @@ describe('render api', () => {
383383
});
384384
});
385385
describe('qwikLoader', () => {
386-
// Right now we always include it in head
387-
it.skip('should render at bottom as fallback', async () => {
388-
const result = await renderToStringAndSetPlatform(<Counter />, {
389-
containerTagName: 'div',
386+
describe('inline', () => {
387+
it('should render at bottom as fallback', async () => {
388+
const result = await renderToStringAndSetPlatform(<Counter />, {
389+
containerTagName: 'div',
390+
qwikLoader: 'inline',
391+
});
392+
const document = createDocument({ html: result.html });
393+
// qwik loader is one before last
394+
const qwikLoaderScriptElement = document.body.firstChild?.lastChild
395+
?.previousSibling as HTMLElement;
396+
expect(qwikLoaderScriptElement?.tagName.toLowerCase()).toEqual('script');
397+
expect(qwikLoaderScriptElement?.getAttribute('id')).toEqual('qwikloader');
398+
// qwik events should be the last script of body
399+
const eventsScriptElement = document.body.lastChild as HTMLElement;
400+
expect(eventsScriptElement.textContent).toContain(
401+
'(window.qwikevents||(window.qwikevents=[]))'
402+
);
390403
});
391-
const document = createDocument({ html: result.html });
392-
// qwik loader is one before last
393-
const qwikLoaderScriptElement = document.body.firstChild?.lastChild
394-
?.previousSibling as HTMLElement;
395-
expect(qwikLoaderScriptElement?.tagName.toLowerCase()).toEqual('script');
396-
expect(qwikLoaderScriptElement?.getAttribute('id')).toEqual('qwikloader');
397-
// qwik events should be the last script of body
398-
const eventsScriptElement = document.body.lastChild as HTMLElement;
399-
expect(eventsScriptElement.textContent).toContain(
400-
'(window.qwikevents||(window.qwikevents=[]))'
401-
);
402-
});
403-
it('should always render', async () => {
404-
const result = await renderToStringAndSetPlatform(<div>static</div>, {
405-
containerTagName: 'div',
406-
qwikLoader: {
407-
include: 'always',
408-
},
404+
it('should not render for static content and auto include', async () => {
405+
const result = await renderToStringAndSetPlatform(<div>static</div>, {
406+
containerTagName: 'div',
407+
qwikLoader: 'inline',
408+
});
409+
const document = createDocument({ html: result.html });
410+
// should not contain qwik events script for top position
411+
expect(document.head.lastChild?.textContent ?? '').not.toContain(
412+
'window.qwikevents.push'
413+
);
414+
expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(0);
409415
});
410-
const document = createDocument({ html: result.html });
411-
// should not contain qwik events script for top position
412-
expect(document.head.lastChild?.textContent ?? '').not.toContain('window.qwikevents');
413-
expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(1);
414-
});
415-
// Right now we always include it
416-
it.skip('should not render for static content and auto include', async () => {
417-
const result = await renderToStringAndSetPlatform(<div>static</div>, {
418-
containerTagName: 'div',
419-
qwikLoader: {
420-
include: 'auto',
421-
},
416+
it('should render after 30kB of SSR', async () => {
417+
const bigText = 'hello world '.repeat(3000); // ~30kB of text
418+
const result = await renderToStringAndSetPlatform(
419+
<div>
420+
<Counter />
421+
<div>{bigText}</div>
422+
<div>{bigText}</div>
423+
</div>,
424+
{
425+
containerTagName: 'div',
426+
qwikLoader: 'inline',
427+
}
428+
);
429+
const document = createDocument({ html: result.html });
430+
expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(1);
431+
const notQwikLoaderScriptElement = document.body.firstChild?.lastChild
432+
?.previousSibling as HTMLElement;
433+
expect(notQwikLoaderScriptElement?.id).not.toEqual('qwikloader');
434+
// qwik events should still be the last script of body
435+
const eventsScriptElement = document.body.lastChild as HTMLElement;
436+
expect(eventsScriptElement.textContent).toContain(
437+
'(window.qwikevents||(window.qwikevents=[]))'
438+
);
422439
});
423-
const document = createDocument({ html: result.html });
424-
// should not contain qwik events script for top position
425-
expect(document.head.lastChild?.textContent ?? '').not.toContain('window.qwikevents.push');
426-
expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(0);
427440
});
428-
it('should never render', async () => {
441+
it('should support never render', async () => {
429442
const result = await renderToStringAndSetPlatform(<Counter />, {
430443
containerTagName: 'div',
431-
qwikLoader: {
432-
include: 'never',
433-
},
444+
qwikLoader: 'never',
434445
});
435446
const document = createDocument({ html: result.html });
436447
expect(document.querySelectorAll('script[id=qwikloader]')).toHaveLength(0);
437-
// should not contain qwik events script for top position
438-
expect(document.head.lastChild?.textContent ?? '').not.toContain('window.qwikevents.push');
439448
});
440449
});
441450
describe('qwikEvents', () => {

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

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface SSRRenderOptions {
114114
enum QwikLoaderInclude {
115115
Module,
116116
Inline,
117-
Never,
117+
Done,
118118
}
119119

120120
export function ssrCreateContainer(opts: SSRRenderOptions): ISSRContainer {
@@ -255,12 +255,12 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
255255
this.qlInclude = qlOpt
256256
? typeof qlOpt === 'object'
257257
? qlOpt.include === 'never'
258-
? QwikLoaderInclude.Never
258+
? QwikLoaderInclude.Done
259259
: QwikLoaderInclude.Module
260260
: qlOpt === 'inline'
261261
? QwikLoaderInclude.Inline
262262
: qlOpt === 'never'
263-
? QwikLoaderInclude.Never
263+
? QwikLoaderInclude.Done
264264
: QwikLoaderInclude.Module
265265
: QwikLoaderInclude.Module;
266266
if (this.qlInclude === QwikLoaderInclude.Module) {
@@ -387,6 +387,11 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
387387
constAttrs?: SsrAttrs | null,
388388
currentFile?: string | null
389389
): string | undefined {
390+
if (this.qlInclude === QwikLoaderInclude.Inline && this.size > 30 * 1024) {
391+
// We waited long enough, on slow connections the page is already partially visible
392+
this.emitQwikLoaderInline();
393+
}
394+
390395
let innerHTML: string | undefined = undefined;
391396
this.lastNode = null;
392397
const isQwikStyle =
@@ -897,6 +902,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
897902

898903
emitQwikLoaderAtTopIfNeeded() {
899904
if (this.qlInclude === QwikLoaderInclude.Module) {
905+
this.qlInclude = QwikLoaderInclude.Done;
900906
// always emit the preload+import. It will probably be used at some point on the site
901907
const qwikLoaderBundle = this.$buildBase$ + this.resolvedManifest.manifest.qwikLoader!;
902908
const linkAttrs = ['rel', 'modulepreload', 'href', qwikLoaderBundle];
@@ -913,31 +919,39 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
913919
}
914920
this.openElement('script', scriptAttrs);
915921
this.closeElement();
916-
} else if (this.qlInclude === QwikLoaderInclude.Inline) {
917-
// TODO send at the end or after 30kB generated
918-
// if at the end, only include when snapshot is not static
919-
const qwikLoaderScript = getQwikLoaderScript({ debug: this.renderOptions.debug });
920-
// module + async lets it run asap without waiting for DOM, even when inline
921-
const scriptAttrs = ['id', 'qwikloader', 'async', true, 'type', 'module'];
922-
if (this.renderOptions.serverData?.nonce) {
923-
scriptAttrs.push('nonce', this.renderOptions.serverData.nonce);
924-
}
925-
this.openElement('script', scriptAttrs);
926-
this.write(qwikLoaderScript);
927-
this.closeElement();
928922
}
929923
}
930924

925+
emitQwikLoaderInline() {
926+
this.qlInclude = QwikLoaderInclude.Done;
927+
// if at the end, only include when snapshot is not static
928+
const qwikLoaderScript = getQwikLoaderScript({ debug: this.renderOptions.debug });
929+
// module + async lets it run asap without waiting for DOM, even when inline
930+
const scriptAttrs = ['id', 'qwikloader', 'async', true, 'type', 'module'];
931+
if (this.renderOptions.serverData?.nonce) {
932+
scriptAttrs.push('nonce', this.renderOptions.serverData.nonce);
933+
}
934+
this.openElement('script', scriptAttrs);
935+
this.write(qwikLoaderScript);
936+
this.closeElement();
937+
}
938+
931939
private emitQwikLoaderAtBottomIfNeeded() {
932-
// emit the used events so the loader can subscribe to them
933-
this.emitQwikEvents(Array.from(this.serializationCtx.$eventNames$, (s) => JSON.stringify(s)));
940+
if (!this.isStatic()) {
941+
if (this.qlInclude !== QwikLoaderInclude.Done) {
942+
this.emitQwikLoaderInline();
943+
}
944+
// emit the used events so the loader can subscribe to them
945+
this.emitQwikEvents(Array.from(this.serializationCtx.$eventNames$, (s) => JSON.stringify(s)));
946+
}
934947
}
935948

936949
private emitQwikEvents(eventNames: string[]) {
937950
if (eventNames.length > 0) {
938-
const scriptAttrs = this.renderOptions.serverData?.nonce
939-
? ['nonce', this.renderOptions.serverData.nonce]
940-
: null;
951+
const scriptAttrs = ['async', true, 'type', 'module'];
952+
if (this.renderOptions.serverData?.nonce) {
953+
scriptAttrs.push('nonce', this.renderOptions.serverData.nonce);
954+
}
941955
this.openElement('script', scriptAttrs);
942956
this.write(`(window.qwikevents||(window.qwikevents=[])).push(`);
943957
this.writeArray(eventNames, ', ');

0 commit comments

Comments
 (0)