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.