Spaces:
Running
Running
File size: 4,550 Bytes
8cf5139 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | "use client";
import { useEffect } from "react";
/**
* VideoPerformance β mounts once in the root layout.
*
* What it does:
* 1. visibilitychange β pauses every <video> on the page when tab goes background,
* resumes only those inside the viewport when tab returns.
* 2. IntersectionObserver β watches every <video> added to the DOM at any time
* (MutationObserver + initial scan). Plays when β₯15% visible, pauses otherwise.
* 3. CSS animation budget β injects a <style> that sets animation-play-state:paused
* on the <html> element when the tab is hidden, stopping ALL CSS animations
* (squad scroll, orb spin, gold shimmer, etc.) with zero JS per-frame cost.
* Removes the rule when the tab returns.
* 4. Reduced-motion respect β if the user has prefers-reduced-motion enabled,
* all CSS animations are permanently paused.
*/
export default function VideoPerformance() {
useEffect(() => {
// ββ 1. Inject animation-pause stylesheet ββββββββββββββββββββββββββββββββββ
const styleEl = document.createElement("style");
styleEl.id = "__beryl_perf__";
styleEl.textContent = `
html.tab-hidden * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}
`;
document.head.appendChild(styleEl);
// ββ 2. Per-video IntersectionObserver βββββββββββββββββββββββββββββββββββββ
const observed = new Set<HTMLVideoElement>();
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
const v = e.target as HTMLVideoElement;
if (e.isIntersecting) {
// Ensure GPU layer is set before playing
if (!v.style.transform) v.style.transform = "translateZ(0)";
if (!v.style.willChange) v.style.willChange = "transform";
v.play().catch(() => {});
} else {
v.pause();
}
});
},
{ threshold: 0.15, rootMargin: "0px 0px 100px 0px" }
);
const observeVideo = (v: HTMLVideoElement) => {
if (observed.has(v)) return;
observed.add(v);
// Force GPU layer on every video we touch
v.style.transform = "translateZ(0)";
v.style.willChange = "transform";
v.style.backfaceVisibility = "hidden";
io.observe(v);
};
// Observe all existing videos
document.querySelectorAll<HTMLVideoElement>("video").forEach(observeVideo);
// Watch for videos added dynamically (e.g. lazy-loaded sections)
const mo = new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node instanceof HTMLVideoElement) observeVideo(node);
if (node instanceof Element) {
node.querySelectorAll<HTMLVideoElement>("video").forEach(observeVideo);
}
});
});
});
mo.observe(document.body, { childList: true, subtree: true });
// ββ 3. Tab visibility β pause ALL / resume visible ββββββββββββββββββββββββ
const onVisibility = () => {
if (document.hidden) {
document.documentElement.classList.add("tab-hidden");
observed.forEach((v) => v.pause());
} else {
document.documentElement.classList.remove("tab-hidden");
observed.forEach((v) => {
const r = v.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) {
v.play().catch(() => {});
}
});
}
};
document.addEventListener("visibilitychange", onVisibility);
// ββ 4. iOS first-gesture unlock βββββββββββββββββββββββββββββββββββββββββββ
const onGesture = () => {
observed.forEach((v) => {
const r = v.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) v.play().catch(() => {});
});
};
document.addEventListener("touchstart", onGesture, { once: true });
return () => {
io.disconnect();
mo.disconnect();
document.removeEventListener("visibilitychange", onVisibility);
document.removeEventListener("touchstart", onGesture);
styleEl.remove();
};
}, []);
return null;
}
|