PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.0
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
xspeed / includes / js / delay-bootstrap.js

delay-bootstrap.js in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.4.0, at includes/js/delay-bootstrap.js

691 lines 30.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Delay bootstrap: the inline script Delay JS prints once, on wp_footer.
3 *
4 * It waits for the visitor's first interaction (or the failsafe timeout),
5 * then runs the replay: every delayed script is put back, in page order,
6 * and each one hears DOMContentLoaded, readystatechange and load once, as
7 * if it had run while the page loaded (#494).
8 *
9 * This is the readable source. `npm run build` minifies it into
10 * assets/delay-bootstrap.min.js with scripts/minify-delay-bootstrap.mjs,
11 * which only minifies: it adds no wrapper, helper or strict-mode line.
12 * Minify_Filters::print_delay_bootstrap() inlines the built copy and puts
13 * the timeout in place of XSPEED_DELAY_TIMEOUT at the very end. Edit this
14 * file, rebuild, and commit both; a unit test fails when the built copy is
15 * stale. Behaviour is pinned in a real browser by
16 * tests/e2e/85-delay-js-replay-harness.spec.ts, and the choice to rename
17 * listeners rather than fire the real events again is ADR 0001.
18 *
19 * Why the replay renames listeners. It runs after the page has loaded, so
20 * DOMContentLoaded and load have already fired, and a delayed script that
21 * sets itself up in one of those listeners is never called. Dispatching the
22 * real events again would run every eager script's handlers a second time.
23 * Instead, while the replay runs, addEventListener/removeEventListener on
24 * document and window file the three lifecycle events under private xs-*
25 * names. Only code running during the replay registers there, so
26 * dispatching the private names reaches exactly the replayed scripts. The
27 * dispatched event reports the real `type`: shared handlers and jQuery's
28 * dispatcher look handlers up by it.
29 *
30 * Properties this sets on script elements (plain properties, so the
31 * minifier leaves their names alone):
32 * - _xs: one of our clones of a delayed script;
33 * - _xe: on the page, and not delayed, when the replay started;
34 * - _xf: written into the page by our document.write redirect;
35 * - _xw: where the next write from that script goes.
36 */
37 (function (timeout) {
38 var TRIGGERS = ['mousemove', 'keydown', 'touchstart', 'scroll', 'wheel'];
39 var PRIVATE = {
40 DOMContentLoaded: 'xs-DOMContentLoaded',
41 load: 'xs-load',
42 readystatechange: 'xs-readystatechange',
43 };
44
45 // The real readyState, read past any getter defined on document itself
46 // (ours during the replay, or one the page defined).
47 var nativeReadyState = Object.getOwnPropertyDescriptor(Document.prototype, 'readyState');
48 function realReadyState() {
49 return nativeReadyState ? nativeReadyState.get.call(document) : document.readyState;
50 }
51
52 // readyState cannot say whether DOMContentLoaded has fired: it turns
53 // 'interactive' BEFORE the defer scripts and modules run. This runs
54 // inline in the footer, before DOMContentLoaded, so a listener records
55 // the real event; Navigation Timing covers a bootstrap that ran later.
56 // The same for load.
57 var started = false;
58 var dclFired = realReadyState() === 'complete';
59 var loadFired = dclFired;
60 var onRealDcl;
61 document.addEventListener('DOMContentLoaded', function () {
62 dclFired = true;
63 if (onRealDcl) onRealDcl();
64 });
65 window.addEventListener('load', function () {
66 loadFired = true;
67 });
68
69 // Dispatch the private copy of a lifecycle event under its real type.
70 // The dispatched load reports document as its target, as the real one
71 // does.
72 function firePrivate(target, type, bubbles) {
73 var event = new Event(PRIVATE[type], { bubbles: !!bubbles });
74 Object.defineProperty(event, 'type', { value: type });
75 if (type === 'load') Object.defineProperty(event, 'target', { value: document });
76 target.dispatchEvent(event);
77 }
78
79 function callHandler(handler, target, type) {
80 if (typeof handler !== 'function') return;
81 try {
82 handler.call(target, new Event(type));
83 } catch (e) {}
84 }
85
86 function typeOf(script) {
87 return (script.getAttribute('type') || '').trim().toLowerCase();
88 }
89
90 // Whether the browser will execute a script of this type. One it will
91 // not run (text/plain, nomodule, text/babel) fires neither load nor
92 // error, so counting it would hold the replay open.
93 function willRun(script, type) {
94 return !script.noModule && /^$|^module$|^(text|application)\/(x-)?(java|ecma|j|live)script$/.test(type);
95 }
96
97 function start() {
98 if (started) return;
99 started = true;
100 TRIGGERS.forEach(function (name) {
101 window.removeEventListener(name, start, { passive: true, capture: true });
102 });
103 var nav = window.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0];
104 if (nav && nav.domContentLoadedEventStart > 0) dclFired = true;
105
106 // pageLoaded: the replay starts after load, the usual case ("late").
107 // phase: 0 until the synthetic DOMContentLoaded, then 1 until load.
108 // pending: what the current phase still waits for; it starts at 1 for
109 // the loop below. heldInlines: inline scripts still waiting behind an
110 // external. modules: inline module index -> 1 inserted, 2 running
111 // between its markers, 0 done.
112 var pageLoaded = loadFired;
113 var live = 1;
114 var wrapped = [];
115 var pending = 1;
116 var deadline;
117 var phase = 0;
118 var heldInlines = 0;
119 var modules = {};
120
121 // When the replay starts before the real DOMContentLoaded, wait for it
122 // before sending our own copy, so a script that registered after it
123 // still gets one.
124 if (!dclFired) {
125 pending++;
126 onRealDcl = function () {
127 onRealDcl = 0;
128 Promise.resolve().then(function () {
129 if (!phase) done();
130 });
131 };
132 }
133
134 // A readystatechange 'complete' still to come is forwarded to the
135 // private name when it happens. This is registered before our wrappers,
136 // so it goes on the real name. A handler a delayed script set on
137 // document.onreadystatechange before the real 'interactive' hears it
138 // from the browser, so the synthetic one skips it.
139 if (!pageLoaded) {
140 document.addEventListener('readystatechange', function () {
141 if (realReadyState() === 'interactive') currentRsc = document.onreadystatechange;
142 if (realReadyState() === 'complete') firePrivate(document, 'readystatechange');
143 });
144 }
145
146 // Scripts already on the page keep the real event names and the native
147 // document.write for their own top-level code: a page defer script's
148 // readystatechange listener hears only the real one, and the browser
149 // ignores their writes after parsing as it always did.
150 Array.prototype.forEach.call(document.scripts, function (script) {
151 if (!script.hasAttribute('data-xs-delay')) script._xe = 1;
152 });
153
154 // The wrappers. What is renamed is decided on each call, by which events
155 // have really fired: DOMContentLoaded once it has, readystatechange once
156 // parsing has finished, window load once the page has loaded. A listener
157 // for an event still to come stays on the real name and hears the real
158 // event, whoever adds it; one for an event already gone gets a private
159 // copy. Renaming everything from the start was worse: eager scripts
160 // still registering had their listeners renamed, jQuery's completed()
161 // ran twice, and the Interactivity API's hydration was held until the
162 // replay ended.
163 //
164 // load is renamed on window only. The page's load never reaches a
165 // listener on document, so one there catches its descendants' loads
166 // in the capture phase (image delegation); renaming it would cut the
167 // delayed script off from every image after this.
168 //
169 // What renaming cannot tell apart: eager code in a timer or handler (no
170 // currentScript) registering during the replay gets one synthetic event
171 // where it would have got none.
172 //
173 // The wrappers look the prototype method up on every call: Sentry or
174 // zone.js, delayed too, can patch EventTarget.prototype during the
175 // replay, and listeners added after that must go through their patch.
176 // A removal takes the listener off both the real and the private name,
177 // so one added before an event fired and removed after (jQuery's
178 // completed()) is still found.
179 [document, window].forEach(function (target) {
180 var proto = Object.getPrototypeOf(target);
181 var ownAdd = target.addEventListener;
182 var ownRemove = target.removeEventListener;
183 var hadOwn = Object.prototype.hasOwnProperty.call(target, 'addEventListener');
184 var nameFor = function (type) {
185 var current = document.currentScript;
186 var gone =
187 type === 'load'
188 ? target === window && loadFired
189 : type === 'DOMContentLoaded'
190 ? dclFired
191 : realReadyState() !== 'loading';
192 return live && !(current && current._xe) && PRIVATE.hasOwnProperty(type) && gone ? PRIVATE[type] : type;
193 };
194 var add = function (type, listener, options) {
195 return (hadOwn ? ownAdd : proto.addEventListener).call(this, nameFor(type), listener, options);
196 };
197 target.addEventListener = add;
198 target.removeEventListener = function (type, listener, options) {
199 var remove = hadOwn ? ownRemove : proto.removeEventListener;
200 if (PRIVATE.hasOwnProperty(type)) remove.call(this, PRIVATE[type], listener, options);
201 return remove.call(this, type, listener, options);
202 };
203 wrapped.push([target, ownAdd, ownRemove, hadOwn, add]);
204 });
205
206 // window.onload / document.onreadystatechange set during the replay are
207 // keyed on the real names, so they are called directly. A page handler
208 // already there is swapped for an empty one first: the old addLoadEvent
209 // chain (`var o=window.onload; window.onload=function(){o();mine();}`)
210 // would otherwise run it a second time. It has run, and the browser
211 // will not call it again. If nothing replaced the empty one, the page's
212 // handler is put back at the end, so code that calls window.onload()
213 // later (a PJAX re-init) still finds it.
214 var pageOnload = window.onload;
215 var pageRsc = document.onreadystatechange;
216 var currentOnload = pageOnload;
217 var currentRsc = pageRsc;
218 var jq = window.jQuery;
219 var ended;
220 if (pageLoaded && pageOnload) currentOnload = window.onload = function () {};
221 if (pageLoaded && pageRsc) currentRsc = document.onreadystatechange = function () {};
222
223 // The replayed scripts read a fake document.readyState: 'loading' while
224 // they run, then 'interactive' for the synthetic DOMContentLoaded, then
225 // the real value once the replay ends. Left at the real value, a script
226 // that starts at once when the page is past 'loading' and also adds an
227 // unguarded DOMContentLoaded listener started twice, and a
228 // readystatechange handler waiting for 'interactive' never ran.
229 //
230 // The fake is scoped: the getter answers with it only while one of our
231 // clones runs (currentScript carries _xs), while an inline module runs
232 // between its markers (modules have no currentScript), and while our own
233 // dispatch runs (fakeDepth). Everything else reads the real value: eager
234 // code, timers, callbacks, and scripts the page or the replayed ones
235 // inject. So a delayed script that listens at top level and re-checks
236 // for 'complete' in a timer starts twice.
237 //
238 // The define is configurable and in a try: WP Rocket's first fake threw
239 // where Cloudflare Rocket Loader had locked the property (#5709), and a
240 // locked readyState keeps the old behaviour here. A getter the page
241 // defined itself (another optimizer, a polyfill) is read through and
242 // put back, not deleted.
243 var writtenPending = 0;
244 var fakeState = 'loading';
245 var fakeDepth = 0;
246 var ownReadyState = Object.getOwnPropertyDescriptor(document, 'readyState');
247 var faking =
248 nativeReadyState &&
249 (function () {
250 try {
251 Object.defineProperty(document, 'readyState', {
252 configurable: true,
253 get: function () {
254 var current = document.currentScript;
255 if ((current && current._xs) || fakeDepth) return fakeState;
256 return ownReadyState && ownReadyState.get ? ownReadyState.get.call(document) : realReadyState();
257 },
258 });
259 return 1;
260 } catch (e) {}
261 })();
262
263 // With readyState faked, a script may write into the page as if it were
264 // still being parsed. From an inline script that would wipe the
265 // document, so for the length of the replay document.write/writeln put
266 // the markup in after the script that wrote it, in call order. A script
267 // the parser is still running writes into the parser as always: while
268 // the real readyState is 'loading', a caller that is not one of our
269 // clones gets the native write. With no currentScript (a timer, a
270 // callback) there is nowhere to put it, and the write is dropped rather
271 // than wiping the page.
272 //
273 // A written <script src> runs whenever it arrives: fragment scripts are
274 // async. One written that way (an ad tag's second stage) writes through
275 // the redirect too, even while the page still parses, and the redirect
276 // stays until every written <script src> has loaded or failed, however
277 // long after the replay that is. Each of write and writeln is put back
278 // only if it is still ours: an ad loader may have installed its own.
279 var nativeWrite = [
280 document.write,
281 document.writeln,
282 Object.prototype.hasOwnProperty.call(document, 'write'),
283 ];
284 function redirectWrite(args, end) {
285 var current = document.currentScript;
286 var html = Array.prototype.join.call(args, '') + end;
287 if (!current) return;
288 if (current._xe || (!current._xs && !current._xf && realReadyState() === 'loading')) {
289 return (end ? nativeWrite[1] : nativeWrite[0]).apply(document, args);
290 }
291 if (!current.parentNode) return;
292 if (!('_xw' in current)) current._xw = current.nextSibling;
293 try {
294 var fragment = document.createRange().createContextualFragment(html);
295 Array.prototype.forEach.call(fragment.querySelectorAll('script'), function (script) {
296 script._xf = 1;
297 if (!script.src) return;
298 writtenPending++;
299 var settled = 0;
300 var settle = function () {
301 if (settled) return;
302 settled = 1;
303 writtenPending--;
304 if (ended && !heldInlines && !writtenPending) restoreWrite();
305 };
306 script.addEventListener('load', settle);
307 script.addEventListener('error', settle);
308 });
309 current.parentNode.insertBefore(fragment, current._xw);
310 } catch (e) {}
311 }
312 var ourWrite = (document.write = function () {
313 redirectWrite(arguments, '');
314 });
315 var ourWriteln = (document.writeln = function () {
316 redirectWrite(arguments, '\n');
317 });
318 function restoreWrite() {
319 if (document.write === ourWrite) {
320 if (nativeWrite[2]) document.write = nativeWrite[0];
321 else delete document.write;
322 }
323 if (document.writeln === ourWriteln) {
324 if (nativeWrite[2]) document.writeln = nativeWrite[1];
325 else delete document.writeln;
326 }
327 }
328
329 function fireReadyStateChange() {
330 firePrivate(document, 'readystatechange');
331 if (document.onreadystatechange !== currentRsc) {
332 callHandler(document.onreadystatechange, document, 'readystatechange');
333 }
334 }
335
336 // jQuery keeps ONE native listener per element and type. If window load
337 // already had jQuery handlers before the replay, that listener is on the
338 // real name and the private dispatch cannot reach handlers appended to
339 // the list, so those, and only those, are called directly in finish().
340 function jqueryLoadHandlers() {
341 var events = jq && jq._data && jq._data(window, 'events');
342 return (events && events.load) || [];
343 }
344 var jqueryLoadCount = jqueryLoadHandlers().length;
345
346 // Called once per thing the current phase waits for. Order matches a
347 // real page: readystatechange ('interactive'), DOMContentLoaded
348 // (bubbles document -> window, so it is not also dispatched on window),
349 // readystatechange ('complete'), load.
350 //
351 // Between DOMContentLoaded and load there is a second wait. On a real
352 // page a script inserted before load delays it, so a script that a
353 // DOMContentLoaded handler or jQuery ready code injects (GTM's DOM Ready
354 // trigger) always hears load. So the observer stays on, and load waits
355 // for what it sees. A wait left over from the first phase (the deadline
356 // cut it short) is ignored when it ends, and so are module markers.
357 //
358 // load also waits for jQuery's ready callbacks. On a real page ready
359 // runs before load, so `jQuery(function(){ $(window).on('load', f) })`,
360 // the common WordPress pattern, gets f. jQuery 3 runs ready callbacks on
361 // timers, so a callback queued after the delayed scripts' own, then one
362 // more timer, is when they have all run. 1s caps it (jQuery.holdReady
363 // can hold ready indefinitely); the callback and the cap share one slot.
364 function done() {
365 if (pending < 1 || --pending) return;
366 if (phase) return finish();
367 phase = 1;
368 clearTimeout(deadline);
369 for (var i in modules) if (modules[i] === 2) fakeDepth--;
370 modules = {};
371 if (faking) {
372 fakeState = 'interactive';
373 fakeDepth++;
374 fireReadyStateChange();
375 }
376 firePrivate(document, 'DOMContentLoaded', 1);
377 if (faking) fakeDepth--;
378 if (pageLoaded && !faking) fireReadyStateChange();
379 pending = 1;
380 var $ = window.jQuery;
381 var once = 0;
382 var ready = function () {
383 if (!once) {
384 once = 1;
385 done();
386 }
387 };
388 try {
389 if ($ && $.fn && $.fn.ready) {
390 $(function () {
391 setTimeout(ready, 0);
392 });
393 setTimeout(ready, 1000);
394 return;
395 }
396 } catch (e) {}
397 setTimeout(ready, 0);
398 }
399
400 // Restore puts back what was there (delete our own-property shadow, or
401 // reassign a shadow someone set before us) unless another wrapper has
402 // since been put on top; `live` makes ours inert either way.
403 function finish() {
404 if (ended) return;
405 ended = 1;
406 if (observer) observer.disconnect();
407 if (faking) {
408 if (ownReadyState) Object.defineProperty(document, 'readyState', ownReadyState);
409 else delete document.readyState;
410 if (pageLoaded) fireReadyStateChange();
411 }
412 if (!heldInlines && !writtenPending) restoreWrite();
413 live = false;
414 wrapped.forEach(function (entry) {
415 var target = entry[0];
416 if (target.addEventListener !== entry[4]) return;
417 if (entry[3]) {
418 target.addEventListener = entry[1];
419 target.removeEventListener = entry[2];
420 } else {
421 delete target.addEventListener;
422 delete target.removeEventListener;
423 }
424 });
425 if (loadFired) firePrivate(window, 'load');
426 if (pageLoaded) {
427 if (window.onload !== currentOnload) callHandler(window.onload, window, 'load');
428 if (jqueryLoadCount) {
429 jqueryLoadHandlers()
430 .slice(jqueryLoadCount)
431 .forEach(function (handleObj) {
432 try {
433 var event = jq.Event('load');
434 event.currentTarget = window;
435 event.handleObj = handleObj;
436 event.data = handleObj.data;
437 handleObj.handler.call(window, event);
438 } catch (e) {}
439 });
440 }
441 }
442 if (pageOnload && window.onload === currentOnload) window.onload = pageOnload;
443 if (pageLoaded && pageRsc && document.onreadystatechange === currentRsc) document.onreadystatechange = pageRsc;
444 // Marks the end, for tests and integrators.
445 document.dispatchEvent(new Event('xspeed:replayed'));
446 }
447
448 // Wait for a script's load or error: listeners, never n.onload, which
449 // would replace an author's copied onload attribute. They go on before
450 // insertion, which is what starts the fetch.
451 function wait(script, cap) {
452 pending++;
453 var settled = 0;
454 var timer;
455 var inPhase = phase;
456 function one() {
457 if (settled) return;
458 settled = 1;
459 clearTimeout(timer);
460 if (!cap && !phase) armDeadline();
461 if (inPhase === phase) done();
462 }
463 script.addEventListener('load', one);
464 script.addEventListener('error', one);
465 if (cap) timer = setTimeout(one, cap);
466 }
467
468 // A script a replayed script injects (a tag manager's payload, a widget's
469 // real code) is waited for too, 1s each: the cap WP Rocket and WP Meteor
470 // use, because some never fire load or error (one added through
471 // innerHTML never runs). A script injected later than that gets no
472 // events. Our own clones carry _xs, so they are not counted twice.
473 function seen(script) {
474 if (!script._xs && script.hasAttribute('src') && willRun(script, typeOf(script))) wait(script, 1000);
475 }
476 var observer =
477 window.MutationObserver &&
478 new MutationObserver(function (records) {
479 records.forEach(function (record) {
480 Array.prototype.forEach.call(record.addedNodes, function (node) {
481 if (node.nodeName === 'SCRIPT') seen(node);
482 else if (node.querySelectorAll) Array.prototype.forEach.call(node.querySelectorAll('script'), seen);
483 });
484 });
485 });
486 if (observer) observer.observe(document.documentElement, { childList: true, subtree: true });
487
488 // A server that never answers does not hold DOMContentLoaded past 15s
489 // with no progress. Each replayed external that loads or fails in the
490 // first phase restarts the 15s, so the whole wait is at most 15s per
491 // external. A fixed 15s from the first interaction sent the events while
492 // a slow connection was still bringing in jQuery and the builder scripts,
493 // and those then missed them for good. An injected script has its own 1s
494 // cap and restarts nothing: the observer also sees the page's own
495 // scripts, and a page that keeps adding them (an ad refresh, a carousel
496 // cloning a slide with a script in it) would hold the events forever.
497 function armDeadline() {
498 clearTimeout(deadline);
499 deadline = setTimeout(function () {
500 pending = 1;
501 done();
502 }, 15000);
503 }
504 armDeadline();
505
506 // An inline module evaluates asynchronously but fires no load (the spec
507 // fires it only for scripts from a URL), so its replayed text starts and
508 // ends with a line that dispatches xs-mod with its index, and the end is
509 // counted in pending. Its imports are hoisted, so the end runs once the
510 // module and its whole import graph have run. An import that fails to
511 // load fires error on the element, which also counts.
512 //
513 // A module that throws never reaches its end line, but its start line
514 // ran and the throw is reported to window: an error while a module is
515 // between its markers releases that module only. The release runs one
516 // microtask after the error: an error thrown by another listener while
517 // the module is still running is reported synchronously, and by the
518 // microtask that module has reached its end line. A module paused at a
519 // top-level await is between its markers too, so an unrelated error
520 // during the pause releases it early; a precise rule costs more than it
521 // saves. A dependency that throws stops the module before its start
522 // line; the deadline covers that. A module's end is counted a microtask
523 // later, so a script it injects is seen first.
524 function moduleDone(index) {
525 if (modules[index]) {
526 if (modules[index] === 2) fakeDepth--;
527 modules[index] = 0;
528 Promise.resolve().then(done);
529 }
530 }
531 document.addEventListener('xs-mod', function (event) {
532 var detail = event.detail;
533 if (detail > 0) moduleDone(detail);
534 else if (modules[-detail] === 1) {
535 modules[-detail] = 2;
536 fakeDepth++;
537 }
538 });
539 window.addEventListener('error', function () {
540 Promise.resolve().then(function () {
541 for (var i in modules) if (modules[i] === 2) moduleDone(i);
542 });
543 });
544
545 // Build the executable copy of a parked tag. index is its 1-based place
546 // in the loop, or 0 for a copy nothing waits for.
547 function clone(parked, index) {
548 var script = document.createElement('script');
549 script._xs = 1;
550 // A dynamically-created script is async by default, so replayed
551 // externals would race each other; async=false restores document
552 // order among them.
553 script.async = false;
554 // Nonce hiding: a connected element's nonce content attribute reads as
555 // "", so copying it through the attribute loop would hand the clone an
556 // empty nonce and a nonce CSP would block it. The IDL property still
557 // carries the real value.
558 if (parked.nonce) script.nonce = parked.nonce;
559 Array.prototype.slice.call(parked.attributes).forEach(function (attr) {
560 if (attr.name === 'data-xs-src') {
561 script.setAttribute('src', attr.value);
562 return;
563 }
564 if (attr.name === 'data-xs-delay') return;
565 if (attr.name === 'nonce') return;
566 // A parked inline tag's original type (module, mostly) rides in
567 // data-xs-type: restore it, or a module runs as a classic script and
568 // its imports throw (#274).
569 if (attr.name === 'data-xs-type') {
570 script.setAttribute('type', attr.value);
571 return;
572 }
573 // type is what a script IS, so it is carried over, except our own
574 // parking marker, which exists only to stop the browser running the
575 // original. Dropping type wholesale made type="module" a classic
576 // script and made a consent manager's type="text/plain" executable
577 // again, which is a privacy failure (#274).
578 if (attr.name === 'type' && attr.value === 'text/xspeed-delayed') return;
579 script.setAttribute(attr.name, attr.value);
580 });
581 if (!parked.hasAttribute('data-xs-src')) script.text = parked.text;
582 var type = typeOf(script);
583 if (index && willRun(script, type)) {
584 if (script.hasAttribute('src')) wait(script);
585 else if (type === 'module') {
586 pending++;
587 modules[index] = 1;
588 script.text =
589 "document.dispatchEvent(new CustomEvent('xs-mod',{detail:-" +
590 index +
591 '}));' +
592 script.text +
593 "\n;document.dispatchEvent(new CustomEvent('xs-mod',{detail:" +
594 index +
595 '}))';
596 script.addEventListener('error', function () {
597 moduleDone(index);
598 });
599 // A CSP that lists inline scripts by hash blocks the changed text,
600 // so on an enforced violation the untouched original goes in
601 // instead, unwaited for. Report-only violations are ignored: that
602 // copy ran.
603 script.addEventListener('securitypolicyviolation', function (event) {
604 if (event.disposition === 'enforce' && event.blockedURI === 'inline' && script.parentNode) {
605 script.parentNode.replaceChild(clone(parked), script);
606 moduleDone(index);
607 }
608 });
609 }
610 }
611 return script;
612 }
613
614 // The loop. An inline script runs the moment it is inserted, so one
615 // after a delayed external would run before that external arrived
616 // (`jQuery(function(){...})` after a delayed jQuery threw). An inline
617 // that follows a classic, non-async, non-defer executable external is
618 // inserted from that external's load/error listener instead. The
619 // async=false externals run from one ordered list, and the spec fires
620 // each one's load right after it runs and before the next starts, so the
621 // inline runs exactly between them; the externals are still all inserted
622 // at once, so they download in parallel. An async or defer external and
623 // a module hold nothing: on a real page none of them blocks the inline
624 // after it.
625 //
626 // A held inline's text is unchanged, so a hash CSP still allows it. Its
627 // hold is released a microtask after it goes in, so a script it injects
628 // is counted first. A hold released after the deadline no longer counts
629 // toward any wait, but the write redirect stays until the last one has
630 // gone in. Each tag is cloned and inserted inside its own try, so one
631 // the browser refuses costs that script only.
632 //
633 // A parked tag an earlier replayed script took out of the document is
634 // skipped: with no parent, replaceChild throws, and inside a detached
635 // subtree the clone never loads. contains(), not isConnected, which
636 // older engines lack.
637 var lastBlocking;
638 document.querySelectorAll('script[data-xs-delay]').forEach(function (parked, k) {
639 if (!document.documentElement.contains(parked)) return;
640 if (lastBlocking && !parked.hasAttribute('data-xs-src')) {
641 pending++;
642 heldInlines++;
643 var released = 0;
644 var inPhase = phase;
645 var release = function () {
646 if (released) return;
647 released = 1;
648 heldInlines--;
649 try {
650 if (document.documentElement.contains(parked)) {
651 parked.parentNode.replaceChild(clone(parked, inPhase === phase ? k + 1 : 0), parked);
652 }
653 } catch (e) {}
654 if (ended && !heldInlines && !writtenPending) restoreWrite();
655 if (inPhase === phase) Promise.resolve().then(done);
656 };
657 lastBlocking.addEventListener('load', release);
658 lastBlocking.addEventListener('error', release);
659 return;
660 }
661 try {
662 var script = clone(parked, k + 1);
663 parked.parentNode.replaceChild(script, parked);
664 } catch (e) {
665 return;
666 }
667 var type = typeOf(script);
668 if (
669 script.hasAttribute('src') &&
670 type !== 'module' &&
671 !script.async &&
672 !script.hasAttribute('defer') &&
673 willRun(script, type)
674 ) {
675 lastBlocking = script;
676 }
677 });
678 // An inline loader (GTM, gtag, the Meta pixel) injects during the loop,
679 // and the observer's records for it arrive in the microtask queued
680 // before this one.
681 Promise.resolve().then(done);
682 }
683
684 TRIGGERS.forEach(function (name) {
685 window.addEventListener(name, start, { passive: true, capture: true });
686 });
687 // The failsafe timer, for visitors who never interact. 0 means
688 // interaction only.
689 if (timeout > 0) setTimeout(start, timeout);
690 })(XSPEED_DELAY_TIMEOUT);
691