Skip to content

Commit 031eeb4

Browse files
authored
Merge pull request #7519 from QwikDev/minify-qwikloader
perf: better minify qwikloader
2 parents aa595d4 + 6cc08d4 commit 031eeb4

File tree

7 files changed

+108
-149
lines changed

7 files changed

+108
-149
lines changed

packages/qwik/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,6 @@
127127
},
128128
"./qwikloader.js": "./dist/qwikloader.js",
129129
"./qwikloader.debug.js": "./dist/qwikloader.debug.js",
130-
"./qwik-prefetch.js": "./dist/qwik-prefetch.js",
131-
"./qwik-prefetch.debug.js": "./dist/qwik-prefetch.debug.js",
132130
"./package.json": "./dist/package.json"
133131
},
134132
"files": [

packages/qwik/src/qwikloader-entry.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/qwik/src/qwikloader-prefetch.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/qwik/src/qwikloader.ts

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import type { QwikSymbolEvent, QwikVisibleEvent } from './core/render/jsx/types/
22
import type { QContainerElement } from './core/container/container';
33
import type { QContext } from './core/state/context';
44

5+
type qWindow = Window & {
6+
qwikevents: {
7+
events: Set<string>;
8+
roots: Set<Node>;
9+
push: (...e: (string | (EventTarget & ParentNode))[]) => void;
10+
};
11+
};
12+
513
/**
614
* Set up event listening for browser.
715
*
@@ -11,31 +19,14 @@ import type { QContext } from './core/state/context';
1119
* @param doc - Document to use for setting up global listeners, and to determine all the browser
1220
* supported events.
1321
*/
14-
export const qwikLoader = (
15-
doc: Document & { __q_context__?: [Element, Event, URL] | 0 },
16-
hasInitialized?: number
17-
) => {
18-
const Q_CONTEXT = '__q_context__';
19-
type qWindow = Window & {
20-
qwikevents: {
21-
events: Set<string>;
22-
roots: Set<Node>;
23-
push: (...e: (string | (EventTarget & ParentNode))[]) => void;
24-
};
25-
};
22+
(() => {
23+
const doc = document as Document & { __q_context__?: [Element, Event, URL] | 0 };
2624
const win = window as unknown as qWindow;
2725
const events = new Set<string>();
2826
const roots = new Set<EventTarget & ParentNode>([doc]);
2927

30-
// Some shortenings for minification
31-
const replace = 'replace';
32-
const forEach = 'forEach';
33-
const target = 'target';
34-
const getAttribute = 'getAttribute';
35-
const isConnected = 'isConnected';
36-
const qvisible = 'qvisible';
37-
const Q_JSON = '_qwikjson_';
38-
const qContainerAttr = '[q\\:container]';
28+
let hasInitialized: number;
29+
3930
const nativeQuerySelectorAll = (root: ParentNode, selector: string) =>
4031
Array.from(root.querySelectorAll(selector));
4132
const querySelectorAll = (query: string) => {
@@ -54,19 +45,19 @@ export const qwikLoader = (
5445
const isPromise = (promise: Promise<any>) => promise && typeof promise.then === 'function';
5546

5647
const broadcast = (infix: string, ev: Event, type = ev.type) => {
57-
querySelectorAll('[on' + infix + '\\:' + type + ']')[forEach]((el) =>
48+
querySelectorAll('[on' + infix + '\\:' + type + ']').forEach((el) =>
5849
dispatch(el, infix, ev, type)
5950
);
6051
};
6152

6253
const resolveContainer = (containerEl: QContainerElement) => {
63-
if (containerEl[Q_JSON] === undefined) {
54+
if (containerEl._qwikjson_ === undefined) {
6455
const parentJSON = containerEl === doc.documentElement ? doc.body : containerEl;
6556
let script = parentJSON.lastElementChild;
6657
while (script) {
67-
if (script.tagName === 'SCRIPT' && script[getAttribute]('type') === 'qwik/json') {
68-
containerEl[Q_JSON] = JSON.parse(
69-
script.textContent![replace](/\\x3C(\/?script)/gi, '<$1')
58+
if (script.tagName === 'SCRIPT' && script.getAttribute('type') === 'qwik/json') {
59+
containerEl._qwikjson_ = JSON.parse(
60+
script.textContent!.replace(/\\x3C(\/?script)/gi, '<$1')
7061
);
7162
break;
7263
}
@@ -93,12 +84,12 @@ export const qwikLoader = (
9384
if (element.hasAttribute('stoppropagation:' + eventName)) {
9485
ev.stopPropagation();
9586
}
96-
const ctx = element['_qc_'];
87+
const ctx = element._qc_;
9788
const relevantListeners = ctx && ctx.li.filter((li) => li[0] === attrName);
9889
if (relevantListeners && relevantListeners.length > 0) {
9990
for (const listener of relevantListeners) {
10091
// listener[1] holds the QRL
101-
const results = listener[1].getFn([element, ev], () => element[isConnected])(ev, element);
92+
const results = listener[1].getFn([element, ev], () => element.isConnected)(ev, element);
10293
const cancelBubble = ev.cancelBubble;
10394
if (isPromise(results)) {
10495
await results;
@@ -110,17 +101,17 @@ export const qwikLoader = (
110101
}
111102
return;
112103
}
113-
const attrValue = element[getAttribute](attrName);
104+
const attrValue = element.getAttribute(attrName);
114105
if (attrValue) {
115-
const container = element.closest(qContainerAttr)! as QContainerElement;
116-
const qBase = container[getAttribute]('q:base')!;
117-
const qVersion = container[getAttribute]('q:version') || 'unknown';
118-
const qManifest = container[getAttribute]('q:manifest-hash') || 'dev';
106+
const container = element.closest('[q\\:container]')! as QContainerElement;
107+
const qBase = container.getAttribute('q:base')!;
108+
const qVersion = container.getAttribute('q:version') || 'unknown';
109+
const qManifest = container.getAttribute('q:manifest-hash') || 'dev';
119110
const base = new URL(qBase, doc.baseURI);
120111
for (const qrl of attrValue.split('\n')) {
121112
const url = new URL(qrl, base);
122113
const href = url.href;
123-
const symbol = url.hash[replace](/^#?([^?[|]*).*$/, '$1') || 'default';
114+
const symbol = url.hash.replace(/^#?([^?[|]*).*$/, '$1') || 'default';
124115
const reqTime = performance.now();
125116
let handler: undefined | any;
126117
let importError: undefined | 'sync' | 'async' | 'no-symbol';
@@ -132,7 +123,7 @@ export const qwikLoader = (
132123
handler = ((doc as any)['qFuncs_' + hash] || [])[Number.parseInt(symbol)];
133124
if (!handler) {
134125
importError = 'sync';
135-
error = new Error('sync handler error for symbol: ' + symbol);
126+
error = new Error('sym:' + symbol);
136127
}
137128
} else {
138129
const uri = url.href.split('#')[0];
@@ -155,10 +146,10 @@ export const qwikLoader = (
155146
// break out of the loop if handler is not found
156147
break;
157148
}
158-
const previousCtx = doc[Q_CONTEXT];
159-
if (element[isConnected]) {
149+
const previousCtx = doc.__q_context__;
150+
if (element.isConnected) {
160151
try {
161-
doc[Q_CONTEXT] = [element, ev, url];
152+
doc.__q_context__ = [element, ev, url];
162153
isSync || emitEvent<QwikSymbolEvent>('qsymbol', { ...eventData });
163154
const results = handler(ev, element);
164155
// only await if there is a promise returned
@@ -168,7 +159,7 @@ export const qwikLoader = (
168159
} catch (error) {
169160
emitEvent('qerror', { error, ...eventData });
170161
} finally {
171-
doc[Q_CONTEXT] = previousCtx;
162+
doc.__q_context__ = previousCtx;
172163
}
173164
}
174165
}
@@ -179,7 +170,7 @@ export const qwikLoader = (
179170
doc.dispatchEvent(createEvent<T>(eventName, detail));
180171
};
181172

182-
const camelToKebab = (str: string) => str[replace](/([A-Z])/g, (a) => '-' + a.toLowerCase());
173+
const camelToKebab = (str: string) => str.replace(/([A-Z])/g, (a) => '-' + a.toLowerCase());
183174

184175
/**
185176
* Event handler responsible for processing browser events.
@@ -192,10 +183,10 @@ export const qwikLoader = (
192183
const processDocumentEvent = async (ev: Event) => {
193184
// eslint-disable-next-line prefer-const
194185
let type = camelToKebab(ev.type);
195-
let element = ev[target] as Element | null;
186+
let element = ev.target as Element | null;
196187
broadcast('-document', ev, type);
197188

198-
while (element && element[getAttribute]) {
189+
while (element && element.getAttribute) {
199190
const results = dispatch(element, '', ev, type);
200191
let cancelBubble = ev.cancelBubble;
201192
if (isPromise(results)) {
@@ -223,17 +214,17 @@ export const qwikLoader = (
223214
const riC = win.requestIdleCallback ?? win.setTimeout;
224215
riC.bind(win)(() => emitEvent('qidle'));
225216

226-
if (events.has(qvisible)) {
227-
const results = querySelectorAll('[on\\:' + qvisible + ']');
217+
if (events.has('qvisible')) {
218+
const results = querySelectorAll('[on\\:qvisible]');
228219
const observer = new IntersectionObserver((entries) => {
229220
for (const entry of entries) {
230221
if (entry.isIntersecting) {
231-
observer.unobserve(entry[target]);
232-
dispatch(entry[target], '', createEvent<QwikVisibleEvent>(qvisible, entry));
222+
observer.unobserve(entry.target);
223+
dispatch(entry.target, '', createEvent<QwikVisibleEvent>('qvisible', entry));
233224
}
234225
}
235226
});
236-
results[forEach]((el) => observer.observe(el));
227+
results.forEach((el) => observer.observe(el));
237228
}
238229
}
239230
};
@@ -270,9 +261,9 @@ export const qwikLoader = (
270261
}
271262
};
272263

273-
if (!(Q_CONTEXT in doc)) {
264+
if (!('__q_context__' in doc)) {
274265
// Mark qwik-loader presence but falsy
275-
doc[Q_CONTEXT] = 0;
266+
doc.__q_context__ = 0;
276267
const qwikevents = win.qwikevents;
277268
// If `qwikEvents` is an array, process it.
278269
if (Array.isArray(qwikevents)) {
@@ -287,8 +278,4 @@ export const qwikLoader = (
287278
addEventListener(doc, 'readystatechange', processReadyStateChange);
288279
processReadyStateChange();
289280
}
290-
};
291-
292-
export interface QwikLoaderMessage extends MessageEvent {
293-
data: string[];
294-
}
281+
})();

packages/qwik/src/qwikloader.unit.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { readFileSync } from 'fs';
2+
import { expect, test } from 'vitest';
3+
import compress from 'brotli/compress.js';
4+
import { fileURLToPath } from 'url';
5+
import { dirname, resolve } from 'path';
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
9+
test('qwikloader script', () => {
10+
let qwikLoader: string = '';
11+
try {
12+
qwikLoader = readFileSync(resolve(__dirname, '../dist/qwikloader.js'), 'utf-8');
13+
} catch {
14+
// ignore, we didn't build yet
15+
}
16+
// This is to ensure we are deliberate about changes to qwikloader.
17+
expect(qwikLoader.length).toBeGreaterThan(0);
18+
/**
19+
* Note that the source length can be shorter by using strings in variables and using those to
20+
* dereference objects etc, but that actually results in worse compression
21+
*/
22+
expect(qwikLoader.length).toBeLessThan(3127);
23+
const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 });
24+
expect(compressed.length).toBeLessThan(1429);
25+
expect(qwikLoader).toMatchInlineSnapshot(
26+
`"(()=>{const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),a=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},i=t=>{w(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&i(e)}))},c=t=>t&&"function"==typeof t.then,l=(t,e,n=e.type)=>{a("[on"+t+"\\\\:"+n+"]").forEach((o=>b(o,t,e,n)))},f=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},p=(t,e)=>new CustomEvent(t,{detail:e}),b=async(e,n,o,r=o.type)=>{const s="on"+n+":"+r;e.hasAttribute("preventdefault:"+r)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+r)&&o.stopPropagation();const a=e._qc_,i=a&&a.li.filter((t=>t[0]===s));if(i&&i.length>0){for(const t of i){const n=t[1].getFn([e,o],(()=>e.isConnected))(o,e),r=o.cancelBubble;c(n)&&await n,r&&o.stopPropagation()}return}const l=e.getAttribute(s);if(l){const n=e.closest("[q\\\\:container]"),r=n.getAttribute("q:base"),s=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",i=new URL(r,t.baseURI);for(const p of l.split("\\n")){const l=new URL(p,i),b=l.href,h=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",q=performance.now();let _,d,y;const w=p.startsWith("#"),g={qBase:r,qManifest:a,qVersion:s,href:b,symbol:h,element:e,reqTime:q};if(w){const e=n.getAttribute("q:instance");_=(t["qFuncs_"+e]||[])[Number.parseInt(h)],_||(d="sync",y=Error("sym:"+h))}else{const t=l.href.split("#")[0];try{const e=import(t);f(n),_=(await e)[h],_||(d="no-symbol",y=Error(\`\${h} not in \${t}\`))}catch(t){d||(d="async"),y=t}}if(!_){u("qerror",{importError:d,error:y,...g}),console.error(y);break}const m=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l],w||u("qsymbol",{...g});const n=_(o,e);c(n)&&await n}catch(t){u("qerror",{error:t,...g})}finally{t.__q_context__=m}}}},u=(e,n)=>{t.dispatchEvent(p(e,n))},h=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),q=async t=>{let e=h(t.type),n=t.target;for(l("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type),n=t.bubbles&&!0!==r?n.parentElement:null}},_=t=>{l("-window",t,h(t.type))},d=()=>{var s;const c=t.readyState;if(!r&&("interactive"==c||"complete"==c)&&(o.forEach(i),r=1,u("qinit"),(null!=(s=e.requestIdleCallback)?s:e.setTimeout).bind(e)((()=>u("qidle"))),n.has("qvisible"))){const t=a("[on\\\\:qvisible]"),e=new IntersectionObserver((t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",p("qvisible",n)))}));t.forEach((t=>e.observe(t)))}},y=(t,e,n,o=!1)=>t.addEventListener(e,n,{capture:o,passive:!1}),w=(...t)=>{for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>y(t,r,q,!0))),y(e,r,_,!0),n.add(r)):o.has(r)||(n.forEach((t=>y(r,t,q,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;Array.isArray(r)&&w(...r),e.qwikevents={events:n,roots:o,push:w},y(t,"readystatechange",d),d()}})();"`
27+
);
28+
});

scripts/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export async function build(config: BuildConfig) {
8080
]);
8181

8282
// server bundling must happen after the results from the others
83-
// because it inlines the qwik loader and prefetch scripts
83+
// because it inlines the qwik loader
8484
await Promise.all([submoduleServer(config), submoduleOptimizer(config)]);
8585
}
8686

0 commit comments

Comments
 (0)