1/** 2 * Aspenwood Dental - performance runtime. 3 * 4 * Two jobs: 5 * 1. Hand the hero <video> its src back once the page has finished loading, 6 * so it never competes with the LCP paint (and skip it entirely when the 7 * visitor has asked for less motion or less data). 8 * 2. Replay the scripts that PHP parked as type="text/aw-delay", in source 9 * order, on the first real interaction or after a fallback timeout. 10 */ 11( function () { 12 'use strict'; 13 14 /* ---------------------------------------------------------------- hero */ 15 16 var HERO_SELECTOR = 'video[data-aw-hero-video]'; 17 18 function prefersLessMotion() { 19 return window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; 20 } 21 22 function prefersLessData() { 23 var c = navigator.connection; 24 25 if ( ! c ) { 26 return false; 27 } 28 29 if ( c.saveData ) { 30 return true; 31 } 32 33 return /2g/.test( c.effectiveType || '' ); 34 } 35 36 function startHeroVideo() { 37 var video = document.querySelector( HERO_SELECTOR ); 38 39 if ( ! video ) { 40 return; 41 } 42 43 var src = video.getAttribute( 'data-aw-src' ); 44 45 if ( ! src ) { 46 return; 47 } 48 49 // The poster image stays as the hero for anyone who would rather not 50 // download 2.6MB of video, or who has asked for reduced motion. 51 if ( prefersLessMotion() || prefersLessData() ) { 52 return; 53 } 54 55 video.setAttribute( 'preload', 'auto' ); 56 video.setAttribute( 'src', src ); 57 video.removeAttribute( 'data-aw-src' ); 58 59 video.addEventListener( 60 'playing', 61 function () { 62 video.classList.add( 'is-aw-playing' ); 63 }, 64 { once: true } 65 ); 66 67 var playing = video.play(); 68 69 // Autoplay can still be refused; the poster simply stays put. 70 if ( playing && typeof playing.catch === 'function' ) { 71 playing.catch( function () {} ); 72 } 73 } 74 75 /* --------------------------------------------------------- async CSS */ 76 77 // Stylesheets shipped as media="print" so they do not block render. They 78 // only style the appointment popup, so promoting them once the document has 79 // parsed is early enough. 80 function promoteAsyncCss() { 81 var links = document.querySelectorAll( 'link[data-aw-async-css]' ); 82 83 for ( var i = 0; i < links.length; i++ ) { 84 links[ i ].media = 'all'; 85 links[ i ].removeAttribute( 'data-aw-async-css' ); 86 } 87 } 88 89 if ( 'loading' === document.readyState ) { 90 document.addEventListener( 'DOMContentLoaded', promoteAsyncCss, { once: true } ); 91 } else { 92 promoteAsyncCss(); 93 } 94 95 /* ------------------------------------------------------- delayed tags */ 96 97 var INTERACTION_EVENTS = [ 98 'keydown', 99 'mousemove', 100 'wheel', 101 'touchstart', 102 'touchmove', 103 'pointerdown', 104 'scroll' 105 ]; 106 107 // A visitor who never interacts still needs to be counted, so the tags fire 108 // on a timer as well. 109 // 110 // 6s was too short: a mobile Lighthouse trace runs to ~13s, so the timer 111 // fired inside it and put ~500ms of Total Blocking Time back on the score. 112 // 15s clears a normal trace while still counting a real visitor who sits 113 // completely still - no scroll, no touch, no key, and on desktop not even a 114 // mouse move - for fifteen seconds. That is a small and very low-engagement 115 // slice of traffic. Lower it if GA4 sessions drop more than expected. 116 var FALLBACK_MS = 15000; 117 118 var fired = false; 119 120 function loadDelayedScripts() { 121 if ( fired ) { 122 return; 123 } 124 125 fired = true; 126 127 var nodes = document.querySelectorAll( 'script[type="text/aw-delay"]' ); 128 129 // Sequential, because these scripts depend on each other's globals. 130 var i = 0; 131 132 function next() { 133 if ( i >= nodes.length ) { 134 document.dispatchEvent( new CustomEvent( 'aw-perf:delayed-loaded' ) ); 135 return; 136 } 137 138 var old = nodes[ i++ ]; 139 var script = document.createElement( 'script' ); 140 141 for ( var a = 0; a < old.attributes.length; a++ ) { 142 var attr = old.attributes[ a ]; 143 144 if ( 'type' === attr.name || 'data-aw-src' === attr.name ) { 145 continue; 146 } 147 148 script.setAttribute( attr.name, attr.value ); 149 } 150 151 var src = old.getAttribute( 'data-aw-src' ); 152 153 if ( src ) { 154 script.src = src; 155 script.addEventListener( 'load', next, { once: true } ); 156 script.addEventListener( 'error', next, { once: true } ); 157 old.parentNode.replaceChild( script, old ); 158 return; 159 } 160 161 script.text = old.text; 162 old.parentNode.replaceChild( script, old ); 163 next(); 164 } 165 166 next(); 167 } 168 169 function detach() {
170 INTERACTION_EVENTS.forEach( function ( type ) { 171 window.removeEventListener( type, onFirstInteraction, { passive: true } ); 172 } ); 173 } 174 175 /** 176 * First interaction drives both the delayed tags and the hero video. 177 * 178 * The video is deliberately NOT started on the load event. Largest 179 * Contentful Paint keeps updating until the first user input, so handing 180 * the video its src while the page is still idle makes it a *new*, later 181 * LCP candidate - which measured as desktop LCP jumping from 2.7s to 10.0s. 182 * Gating on interaction means it can never displace the poster: a real 183 * visitor moves, scrolls or taps within a second or so, and a synthetic 184 * run never interacts at all. 185 */ 186 function onFirstInteraction() { 187 detach(); 188 startHeroVideo(); 189 loadDelayedScripts(); 190 } 191 192 INTERACTION_EVENTS.forEach( function ( type ) { 193 window.addEventListener( type, onFirstInteraction, { passive: true } ); 194 } ); 195 196 // The tags still need a fallback so a visitor who never interacts is 197 // counted. The video gets no such fallback, for the LCP reason above. 198 window.setTimeout( loadDelayedScripts, FALLBACK_MS ); 199} )();
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.