Skip to content

Commit c664219

Browse files
committed
feat(qwikloader): unregister unused event types
1 parent a6dddd6 commit c664219

File tree

4 files changed

+46
-19
lines changed

4 files changed

+46
-19
lines changed

packages/qwik/src/qwikloader.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,20 @@ type qWindow = Window & {
4848

4949
const isPromise = (promise: Promise<any>) => promise && typeof promise.then === 'function';
5050

51+
// Give a grace period before unregistering the event listener
52+
let doNotClean = true;
5153
const broadcast = (infix: string, ev: Event, type = ev.type) => {
52-
querySelectorAll('[on' + infix + '\\:' + type + ']').forEach((el) =>
53-
dispatch(el, infix, ev, type)
54-
);
54+
let found = doNotClean;
55+
querySelectorAll('[on' + infix + '\\:' + type + ']').forEach((el) => {
56+
found = true;
57+
dispatch(el, infix, ev, type);
58+
});
59+
if (!found) {
60+
window[infix.slice(1) as 'window' | 'document'].removeEventListener(
61+
type,
62+
infix === '-window' ? processWindowEvent : processDocumentEvent
63+
);
64+
}
5565
};
5666

5767
const resolveContainer = (containerEl: QContainerElement) => {
@@ -209,7 +219,7 @@ type qWindow = Window & {
209219
await results;
210220
}
211221
// if another async handler stopPropagation
212-
cancelBubble =
222+
cancelBubble ||=
213223
cancelBubble || ev.cancelBubble || element.hasAttribute('stoppropagation:' + ev.type);
214224
element = ev.bubbles && cancelBubble !== true ? element.parentElement : null;
215225
}
@@ -251,10 +261,18 @@ type qWindow = Window & {
251261
handler: (ev: Event) => void,
252262
capture = false
253263
) => {
254-
return el.addEventListener(eventName, handler, { capture, passive: false });
264+
el.addEventListener(eventName, handler, { capture, passive: false });
255265
};
256266

267+
let cleanTimer: NodeJS.Timeout;
257268
const processEventOrNode = (...eventNames: (string | (EventTarget & ParentNode))[]) => {
269+
doNotClean = true;
270+
clearTimeout(cleanTimer);
271+
/**
272+
* Give 20s to have nodes appear that use this event. Newly added nodes will have listeners
273+
* attached by the DOM renderer so won't use the qwikloader.
274+
*/
275+
cleanTimer = setTimeout(() => (doNotClean = false), 20_000);
258276
for (const eventNameOrNode of eventNames) {
259277
if (typeof eventNameOrNode === 'string') {
260278
// If it is string we just add the event to window and each of our roots.
@@ -277,13 +295,19 @@ type qWindow = Window & {
277295
}
278296
};
279297

298+
// Only the first qwikloader will handle events
280299
if (!('__q_context__' in doc)) {
281300
// Mark qwik-loader presence but falsy
282301
doc.__q_context__ = 0;
283302
const qwikevents = win.qwikevents;
284303
// If `qwikEvents` is an array, process it.
285-
if (Array.isArray(qwikevents)) {
286-
processEventOrNode(...qwikevents);
304+
if (qwikevents) {
305+
if (Array.isArray(qwikevents)) {
306+
processEventOrNode(...qwikevents);
307+
} else {
308+
// Assume that there will probably be click or input listeners
309+
processEventOrNode('click', 'input');
310+
}
287311
}
288312
// Now rig up `qwikEvents` so we get notified of new registrations by other containers.
289313
win.qwikevents = {

packages/qwik/src/qwikloader.unit.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ test('qwikloader script', () => {
2121
* dereference objects etc, but that actually results in worse compression
2222
*/
2323
const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 });
24-
expect([compressed.length, qwikLoader.length]).toEqual([1421, 3118]);
24+
expect([compressed.length, qwikLoader.length]).toMatchInlineSnapshot(`
25+
[
26+
1508,
27+
3303,
28+
]
29+
`);
2530
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{u("qsymbol",g);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];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()}})();"`
31+
`"(()=>{const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),i=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},a=t=>{g(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&a(e)}))},c=t=>t&&"function"==typeof t.then;let l=!0;const f=(t,e,n=e.type)=>{let o=l;i("[on"+t+"\\\\:"+n+"]").forEach((r=>{o=!0,b(r,t,e,n)})),o||window[t.slice(1)].removeEventListener(n,"-window"===t?d:_)},p=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}}},u=(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 i=e._qc_,a=i&&i.li.filter((t=>t[0]===s));if(a&&a.length>0){for(const t of a){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",i=n.getAttribute("q:manifest-hash")||"dev",a=new URL(r,t.baseURI);for(const f of l.split("\\n")){const l=new URL(f,a),u=l.href,b=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",q=performance.now();let _,d,w;const m=f.startsWith("#"),y={qBase:r,qManifest:i,qVersion:s,href:u,symbol:b,element:e,reqTime:q};if(m){const e=n.getAttribute("q:instance");_=(t["qFuncs_"+e]||[])[Number.parseInt(b)],_||(d="sync",w=Error("sym:"+b))}else{h("qsymbol",y);const t=l.href.split("#")[0];try{const e=import(t);p(n),_=(await e)[b],_||(d="no-symbol",w=Error(\`\${b} not in \${t}\`))}catch(t){d||(d="async"),w=t}}if(!_){h("qerror",{importError:d,error:w,...y}),console.error(w);break}const g=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l];const n=_(o,e);c(n)&&await n}catch(t){h("qerror",{error:t,...y})}finally{t.__q_context__=g}}}},h=(e,n)=>{t.dispatchEvent(u(e,n))},q=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),_=async t=>{let e=q(t.type),n=t.target;for(f("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r||(r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type)),n=t.bubbles&&!0!==r?n.parentElement:null}},d=t=>{f("-window",t,q(t.type))},w=()=>{var s;const c=t.readyState;if(!r&&("interactive"==c||"complete"==c)&&(o.forEach(a),r=1,h("qinit"),(null!=(s=e.requestIdleCallback)?s:e.setTimeout).bind(e)((()=>h("qidle"))),n.has("qvisible"))){const t=i("[on\\\\:qvisible]"),e=new IntersectionObserver((t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",u("qvisible",n)))}));t.forEach((t=>e.observe(t)))}},m=(t,e,n,o=!1)=>{t.addEventListener(e,n,{capture:o,passive:!1})};let y;const g=(...t)=>{l=!0,clearTimeout(y),y=setTimeout((()=>l=!1),2e4);for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>m(t,r,_,!0))),m(e,r,d,!0),n.add(r)):o.has(r)||(n.forEach((t=>m(r,t,_,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;r&&(Array.isArray(r)?g(...r):g("click","input")),e.qwikevents={events:n,roots:o,push:g},m(t,"readystatechange",w),w()}})();"`
2732
);
2833
});

packages/qwik/src/server/render.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export async function renderToStream(
119119
: [];
120120

121121
const includeMode = opts.qwikLoader?.include ?? 'auto';
122-
const positionMode = opts.qwikLoader?.position ?? 'bottom';
123122
const qwikLoaderChunk = resolvedManifest?.manifest.qwikLoader;
124123
let didAddQwikLoader = false;
125124
if (includeMode !== 'never' && qwikLoaderChunk) {
@@ -133,15 +132,6 @@ export async function renderToStream(
133132
);
134133
didAddQwikLoader = true;
135134
}
136-
if (positionMode === 'top') {
137-
// Assume there will be at least click and input handlers
138-
beforeContent.push(
139-
jsx('script', {
140-
// not all ESM browsers support ||=
141-
dangerouslySetInnerHTML: `(window.qwikevents||(window.qwikevents=[])).push('click','input')`,
142-
})
143-
);
144-
}
145135
preloaderPre(buildBase, resolvedManifest, opts.preloader, beforeContent, opts.serverData?.nonce);
146136

147137
const renderTimer = createTimer();

packages/qwik/src/server/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,15 @@ export interface RenderResult {
124124

125125
/** @public */
126126
export interface QwikLoaderOptions {
127+
/**
128+
* Whether to include the qwikloader script in the document. Normally you don't need to worry
129+
* about this, but in case of multi-container apps using different Qwik versions, you might want
130+
* to only enable it on one of the containers.
131+
*
132+
* Defaults to `'auto'`.
133+
*/
127134
include?: 'always' | 'never' | 'auto';
135+
/** @deprecated No longer used, the qwikloader is always loaded as soon as possible */
128136
position?: 'top' | 'bottom';
129137
}
130138

0 commit comments

Comments
 (0)