From 46aff5657711919d04dc5ff2e4e13581dfa7560d Mon Sep 17 00:00:00 2001 From: kgva CMS Date: Sat, 15 Aug 2026 00:24:17 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20smoother=20video=20player=20=E2=80=94?= =?UTF-8?q?=20rAF-driven=20scrub=20bar,=20less=20cold-start=20stutter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scrub fill/time were driven off timeupdate, which browsers fire irregularly (a handful of times a second) — visible as the bar stepping instead of gliding. now driven by requestAnimationFrame while playing (display refresh rate), with a direct sync on pause/seeked so it still tracks immediately when not running.\n\npreload=none meant zero data was fetched until the play button was pressed, causing a visible stall right at playback start. preload=metadata lets the browser prime the connection and read the moov atom ahead of time without fetching the full video for visitors who never press play. --- assets/js/player.js | 24 ++++++++++++++++++++---- layouts/portfolio/single.html | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 1b6f212..1aef517 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -23,14 +23,30 @@ if (big) big.addEventListener("click", toggle); v.addEventListener("click", toggle); - v.addEventListener("play", function () { root.classList.add("is-playing"); if (play) play.textContent = "❚❚"; }); - v.addEventListener("pause", function () { root.classList.remove("is-playing"); if (play) play.textContent = "▶"; }); - v.addEventListener("loadedmetadata", function () { if (time) time.textContent = "00:00 / " + fmt(v.duration); }); - v.addEventListener("timeupdate", function () { + // drive the scrub bar off rAF instead of `timeupdate` — timeupdate only + // fires a handful of times a second (browser-dependent), which made the + // fill visibly step instead of glide; rAF ties it to the display's own + // refresh rate, only running while actually playing. + var raf = null; + function sync() { var p = v.duration ? v.currentTime / v.duration : 0; if (fill) fill.style.width = (p * 100) + "%"; if (time) time.textContent = fmt(v.currentTime) + " / " + fmt(v.duration || 0); + } + function tick() { sync(); raf = requestAnimationFrame(tick); } + + v.addEventListener("play", function () { + root.classList.add("is-playing"); if (play) play.textContent = "❚❚"; + if (!raf) raf = requestAnimationFrame(tick); }); + v.addEventListener("pause", function () { + root.classList.remove("is-playing"); if (play) play.textContent = "▶"; + if (raf) { cancelAnimationFrame(raf); raf = null; } + sync(); // one last sync so the bar lands exactly on the paused frame + }); + v.addEventListener("ended", function () { if (raf) { cancelAnimationFrame(raf); raf = null; } }); + v.addEventListener("loadedmetadata", function () { if (time) time.textContent = "00:00 / " + fmt(v.duration); }); + v.addEventListener("seeked", sync); // scrubbing while paused still needs to reflect immediately if (scrub) scrub.addEventListener("click", function (e) { var r = scrub.getBoundingClientRect(); if (v.duration) v.currentTime = ((e.clientX - r.left) / r.width) * v.duration; diff --git a/layouts/portfolio/single.html b/layouts/portfolio/single.html index 2ecb7a8..b6eecc2 100644 --- a/layouts/portfolio/single.html +++ b/layouts/portfolio/single.html @@ -30,7 +30,7 @@
{{ if $video }}
-