PageSourceSearch

https://events-trovex.web.app/cdn-lazy.js

js events-trovex.web.app collected 2026-10-03 12:23:14 UTC 4,764 bytes, 108 lines download raw bytes

1// Events app - load a heavy CDN library the first time something actually needs it.
2//
3// WHY THIS EXISTS. index.html loaded every library in the <head> as a blocking script, so an
4// invited customer opening an invitation link waited on 223KB gzipped of code they can never
5// reach: jspdf (116KB, staff badge and schedule PDFs) and html5-qrcode (107KB, the staff
6// check-in scanner). Both are render-blocking, so nothing appears on screen until they land.
7//
8// Measured on the deployed site before this change: 520KB gzipped of blocking <head> scripts
9// in front of the first paint, on a page whose whole job is to say "you are invited, can you
10// come".
11//
12// WHAT STAYS IN THE HEAD, deliberately: qrcode.min.js is 7KB and IS on customer paths
13// (pages-public.js draws a QR code on the /confirm route, helpers.js mints member QR codes),
14// so deferring it would add a round trip to a customer path to save almost nothing. Firebase
15// and React are needed by every route including this one. Only the two genuinely staff-only
16// libraries move here.
17//
18// The loader is memoised per library, so ten call sites racing on the same click share one
19// network request and one script tag.
20
21// url: where to get it. global: the window property that proves it arrived.
22const LIBRARIES = {
23  jspdf: {
24    url: 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
25    global: 'jspdf',
26    label: 'the PDF library'
27  },
28  html5Qrcode: {
29    url: 'https://unpkg.com/[email protected]/html5-qrcode.min.js',
30    global: 'Html5Qrcode',
31    label: 'the QR scanner'
32  }
33};
34
35const pending = {};
36
37// Load one library and resolve when its global is really there.
38//
39// Resolving on the script's load event alone is not enough: a CDN that serves an error page
40// with a 200 fires load and leaves the global undefined, and the caller would then fail on a
41// property of undefined somewhere far from here. The global is what we actually need, so the
42// global is what is checked.
43function loadLibrary(key) {
44  const lib = LIBRARIES[key];
45  if (!lib) return Promise.reject(new Error(`Unknown library: ${key}`));
46
47  // Already there: loaded by an earlier call, or still in the <head> of a page we have not
48  // migrated. Either way there is nothing to do.
49  if (window[lib.global]) return Promise.resolve(window[lib.global]);
50
51  if (pending[key]) return pending[key];
52
53  pending[key] = new Promise((resolve, reject) => {
54    // ALWAYS a fresh element, and the failed one is removed on the way out.
55    //
56    // An earlier version reused any tag already carrying this key. That looked like sensible
57    // reuse and was a hang: a tag is only ever still in the DOM here AFTER its load or error
58    // has fired (a load in flight is held by `pending`, which we returned above). A script
59    // element fires those events exactly once, so the retry attached listeners to a dead tag
60    // and, because a tag was found, never set src and never appended anything. No request, no
61    // event, no settle: `await loadJsPdf()` hung for the rest of the page view. The scanner
62    // call sites were worse, since neither .then nor .catch ran and the camera pane just sat
63    // black with nothing logged.
64    const stale = document.querySelector(`script[data-cdn-lazy="${key}"]`);
65    if (stale && stale.remove) stale.remove();
66
67    const script = document.createElement('script');
68
69    // Never leave a failed attempt memoised or in the DOM: the whole point is that pressing
70    // the button again retries properly.
71    const fail = (message) => {
72      delete pending[key];
73      if (script.remove) script.remove();
74      reject(new Error(message));
75    };
76
77    script.addEventListener('load', () => {
78      if (window[lib.global]) resolve(window[lib.global]);
79      else fail(`${lib.label} loaded but did not register itself`);
80    }, { once: true });
81    script.addEventListener('error', () => fail(`Could not load ${lib.label}`), { once: true });
82
83    script.src = lib.url;
84    script.async = true;
85    script.setAttribute('data-cdn-lazy', key);
86    document.head.appendChild(script);
87  });
88
89  return pending[key];
90}
91
92// Returns window.jspdf (the UMD namespace, so callers still destructure { jsPDF } from it).
93export function loadJsPdf() {
94  return loadLibrary('jspdf');
95}
96
97// Returns window.Html5Qrcode (the constructor itself).
98export function loadHtml5Qrcode() {
99  return loadLibrary('html5Qrcode');
100}
101
102// For a call site that needs both the PDF library and the QR code library before it can draw
103// a badge. QRCode is in the head, so this is really "wait for jspdf and check QRCode arrived".
104export async function loadPdfWithQr() {
105  const jspdf = await loadJsPdf();
106  if (!window.QRCode) throw new Error('Could not load the QR code library');
107  return jspdf;
108}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.