● Zion Boggan zionboggan.com ↗

Optimize hero perf: low-res render, pause offscreen, low-power fallback

73f7e3f   Zion Boggan committed on Jun 25, 2026 (1 month ago)
apps/web/src/app/globals.css +11 -0
@@ -69,6 +69,17 @@ body {
font-family: var(--font-instrument), Georgia, serif;
}
+.hero-fallback {
+ background:
+ radial-gradient(
+ 120% 90% at 70% 45%,
+ rgba(200, 162, 76, 0.16) 0%,
+ rgba(200, 162, 76, 0.05) 30%,
+ transparent 60%
+ ),
+ var(--stage);
+}
+
.beat-dot {
position: fixed;
top: 0;
apps/web/src/components/hero-canvas.tsx +41 -6
@@ -1,22 +1,57 @@
"use client";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
const HeroScene = dynamic(() => import("./hero-scene"), { ssr: false });
+// Renders the WebGL hero only when it is on screen and the tab is visible, on
+// capable devices. Reduced-motion and low-core machines get a static CSS glow.
export function HeroCanvas() {
- const [show, setShow] = useState(false);
+ const wrap = useRef<HTMLDivElement>(null);
+ const [enabled, setEnabled] = useState(false);
+ const [active, setActive] = useState(true);
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
- if (!reduce) setShow(true);
+ const lowPower = (navigator.hardwareConcurrency || 8) <= 4;
+ if (reduce || lowPower) return;
+ setEnabled(true);
+
+ const el = wrap.current;
+ if (!el) return;
+ let inView = true;
+ let hidden = false;
+ const update = () => setActive(inView && !hidden);
+
+ const io = new IntersectionObserver(
+ ([e]) => {
+ inView = e.isIntersecting;
+ update();
+ },
+ { threshold: 0.01 },
+ );
+ io.observe(el);
+
+ const onVis = () => {
+ hidden = document.hidden;
+ update();
+ };
+ document.addEventListener("visibilitychange", onVis);
+
+ return () => {
+ io.disconnect();
+ document.removeEventListener("visibilitychange", onVis);
+ };
}, []);
- if (!show) return null;
return (
- <div className="pointer-events-none absolute inset-0 z-0 opacity-90">
- <HeroScene />
+ <div ref={wrap} className="pointer-events-none absolute inset-0 z-0">
+ {enabled ? (
+ <HeroScene active={active} />
+ ) : (
+ <div className="hero-fallback absolute inset-0" />
+ )}
</div>
);
}
apps/web/src/components/hero-scene.tsx +15 -17
@@ -13,9 +13,9 @@ const vertex = /* glsl */ `
`;
// Stomp shockwave stage-floor. Concentric gold rings emanate on a beat and
-// react to the cursor, on the warm-black stage. One signature object, subtle.
+// react to the cursor, on the warm-black stage. Kept cheap: 3 bands, no grain.
const fragment = /* glsl */ `
- precision highp float;
+ precision mediump float;
varying vec2 vUv;
uniform float uTime;
uniform vec2 uMouse;
@@ -24,10 +24,6 @@ const fragment = /* glsl */ `
const vec3 STAGE = vec3(0.059, 0.055, 0.047);
const vec3 GOLD = vec3(0.784, 0.635, 0.298);
- float hash(vec2 p) {
- return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
- }
-
void main() {
vec2 p = vUv - 0.5;
p.x *= uAspect;
@@ -39,23 +35,20 @@ const fragment = /* glsl */ `
float rings = 0.0;
float t = uTime * 0.42;
- for (int i = 0; i < 4; i++) {
- float phase = fract(t + float(i) * 0.25);
+ for (int i = 0; i < 3; i++) {
+ float phase = fract(t + float(i) * 0.333);
float radius = phase * 1.05;
- float band = smoothstep(0.018, 0.0, abs(d - radius));
- rings += band * (1.0 - phase);
+ rings += smoothstep(0.02, 0.0, abs(d - radius)) * (1.0 - phase);
}
rings *= beat;
float md = length(p - m);
float cursor = smoothstep(0.32, 0.0, md);
- float cursorRing = smoothstep(0.01, 0.0, abs(md - 0.14)) * 0.6;
+ float cursorRing = smoothstep(0.012, 0.0, abs(md - 0.14)) * 0.6;
float energy = rings * 0.55 + cursor * 0.28 + cursorRing;
vec3 col = STAGE + GOLD * energy * 0.6;
-
col *= smoothstep(1.25, 0.15, d);
- col += (hash(vUv * 900.0 + uTime) - 0.5) * 0.012;
gl_FragColor = vec4(col, 1.0);
}
@@ -79,14 +72,14 @@ function Floor() {
const onMove = (e: MouseEvent) => {
target.current.set(e.clientX / window.innerWidth, 1 - e.clientY / window.innerHeight);
};
- window.addEventListener("mousemove", onMove);
+ window.addEventListener("mousemove", onMove, { passive: true });
return () => window.removeEventListener("mousemove", onMove);
}, []);
useFrame((_, dt) => {
const u = mat.current?.uniforms;
if (!u) return;
- u.uTime.value += dt;
+ u.uTime.value += Math.min(dt, 0.05);
u.uAspect.value = size.width / size.height;
const m = u.uMouse.value as THREE.Vector2;
m.x += (target.current.x - m.x) * 0.06;
@@ -106,9 +99,14 @@ function Floor() {
);
}
-export default function HeroScene() {
+export default function HeroScene({ active = true }: { active?: boolean }) {
return (
- <Canvas dpr={[1, 2]} gl={{ antialias: true }} style={{ width: "100%", height: "100%" }}>
+ <Canvas
+ dpr={0.75}
+ frameloop={active ? "always" : "never"}
+ gl={{ antialias: false, powerPreference: "low-power" }}
+ style={{ width: "100%", height: "100%" }}
+ >
<Floor />
</Canvas>
);