patx/demo

optimize mobile scroll

Commit 8ebb1ca · patx · 2026-08-16T02:34:11-04:00

Changeset
8ebb1ca83f43515b67f33afa761bfadf6914328f
Parents
f642a6224b26a5aff04770266dfac3364d283bf5

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/docs/voldservices/hero-scrub-mobile.mp4 b/docs/voldservices/hero-scrub-mobile.mp4
new file mode 100644
index 0000000..0410b21
Binary files /dev/null and b/docs/voldservices/hero-scrub-mobile.mp4 differ
diff --git a/docs/voldservices/index.html b/docs/voldservices/index.html
index a579466..2be7ff9 100644
--- a/docs/voldservices/index.html
+++ b/docs/voldservices/index.html
@@ -1270,8 +1270,9 @@
           <video
             id="hero-video"
             poster="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg"
-            muted playsinline preload="auto"
+            muted playsinline webkit-playsinline autoplay preload="auto"
             aria-hidden="true" tabindex="-1">
+            <source media="(max-width: 720px)" src="https://gitman.io/patx/demo/raw/docs/voldservices/hero-scrub-mobile.mp4" type="video/mp4" />
             <source src="https://gitman.io/patx/demo/raw/docs/voldservices/hero-scrub.mp4" type="video/mp4" />
           </video>
         </div>
@@ -1926,6 +1927,8 @@
       var heroMedia = heroSticky.querySelector('.hero-media');
       var heroVideo = document.getElementById('hero-video');
       var heroVideoReady = false;
+      var heroVideoPrimed = false;
+      var heroVideoPrimePending = false;
       var heroVideoVisible = false;
       var heroVideoRevealPending = false;
       var heroVideoProgress = 0;
@@ -1984,11 +1987,57 @@
         }
       }
 
-      heroVideo.addEventListener('loadedmetadata', function () {
+      function finishHeroVideoPrime() {
         heroVideo.pause();
+        heroVideo.autoplay = false;
+        heroVideo.removeAttribute('autoplay');
+        heroVideoPrimePending = false;
+        heroVideoPrimed = true;
         heroVideoReady = true;
+        window.removeEventListener('touchstart', primeHeroVideo);
         syncHeroVideo(heroVideoProgress, true);
-      });
+      }
+
+      function usePausedSeekFallback() {
+        heroVideo.pause();
+        heroVideo.autoplay = false;
+        heroVideo.removeAttribute('autoplay');
+        heroVideoPrimePending = false;
+        heroVideoReady = true;
+        syncHeroVideo(heroVideoProgress, true);
+      }
+
+      // iOS may load metadata for a muted video without activating its decoder.
+      // A brief hidden play/pause primes frame presentation so later paused
+      // currentTime changes repaint while the user scrolls. Muted inline play
+      // is allowed automatically; the first touch provides a second chance
+      // when Low Power Mode or a browser policy blocks autoplay.
+      function primeHeroVideo() {
+        if (reduceMotion || heroVideoPrimed || heroVideoPrimePending || heroVideo.readyState < 1) return;
+
+        heroVideoPrimePending = true;
+        heroVideo.muted = true;
+        heroVideo.defaultMuted = true;
+
+        var playAttempt = heroVideo.play();
+        if (playAttempt && typeof playAttempt.then === 'function') {
+          playAttempt.then(finishHeroVideoPrime).catch(usePausedSeekFallback);
+        } else {
+          finishHeroVideoPrime();
+        }
+      }
+
+      if (reduceMotion) {
+        heroVideo.autoplay = false;
+        heroVideo.removeAttribute('autoplay');
+        heroVideo.pause();
+      } else {
+        heroVideo.addEventListener('loadedmetadata', primeHeroVideo);
+        window.addEventListener('touchstart', primeHeroVideo, { passive: true });
+
+        // Cached media can reach metadata before this bottom-of-page script runs.
+        if (heroVideo.readyState >= 1) primeHeroVideo();
+      }
 
       // If the user scrolls again while a seek is being decoded, catch the
       // playhead up to the most recent position as soon as that frame lands.
@@ -1999,6 +2048,8 @@
 
       heroVideo.addEventListener('error', function () {
         heroVideoReady = false;
+        heroVideoPrimed = false;
+        heroVideoPrimePending = false;
         heroVideoVisible = false;
         heroVideoRevealPending = false;
         heroMedia.classList.remove('is-video-ready');