Mobile scroll perf: cut paint cost and re-render rate
Mobile scrolling was janky from a stack of expensive paint passes running every frame. This pass: - CSS: hide .grain (SVG feTurbulence is the single biggest cost), drop mix-blend-mode on .haze/.smoke-puff, drop backdrop-filter on .cmd-box, hide the blurred arm-glow, and remove blur filters from chip-glow / conveyor-pulse / scan-tear under <=760px. - JSX: capture an IS_MOBILE flag once at boot. On mobile, halve smoke puffs / chimneys / sparks / chip-pulse dots / embers, and switch GSAP scrub from 0.6 (rAF interpolation) to true (direct scroll-tied) — which is cheaper and more responsive. - HUD: gate setProgress() on a changed integer depth (0..100) so we re-render at most ~100 times per full scroll instead of every frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
33
app.jsx
33
app.jsx
@@ -3,6 +3,12 @@
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
// Mobile heuristic — captured once at boot. Used to skip expensive
|
||||
// effects (extra particles, heavy scrub smoothing) on phones.
|
||||
const IS_MOBILE =
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(max-width: 760px), (pointer: coarse) and (max-width: 900px)").matches;
|
||||
|
||||
/* ───────────────── PixelLogo ─────────────────
|
||||
Bitmap glyphs rendered as a CSS grid of <span>s, so we control individual
|
||||
pixel colors / glow / animations. Original blocky design (no copyrighted mark). */
|
||||
@@ -186,7 +192,7 @@ function PixelLogoStatic({ word, scale, gap, color }) {
|
||||
Each puff is a chunky, low-res-feeling translucent disc that drifts up + fades. */
|
||||
function SmokeLayer({ chimneys, scale = 1, intensity = 1 }) {
|
||||
// Each chimney emits N puffs on staggered phases.
|
||||
const PUFFS_PER = 3;
|
||||
const PUFFS_PER = IS_MOBILE ? 2 : 3;
|
||||
const puffs = [];
|
||||
chimneys.forEach((ch, ci) => {
|
||||
for (let p = 0; p < PUFFS_PER; p++) {
|
||||
@@ -269,7 +275,8 @@ function SparkLayer() {
|
||||
const sparks = useRef(null);
|
||||
if (!sparks.current) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const count = IS_MOBILE ? 7 : 14;
|
||||
for (let i = 0; i < count; i++) {
|
||||
arr.push({
|
||||
a: -90 + (Math.random() - 0.5) * 160, // angle deg
|
||||
d: 18 + Math.random() * 36, // distance
|
||||
@@ -310,7 +317,7 @@ function SparkLayer() {
|
||||
Subtle pulsing orange dots over the PCB area. */
|
||||
function ChipPulses() {
|
||||
// hand-placed % over the chip image
|
||||
const dots = [
|
||||
const allDots = [
|
||||
{ x: 76, y: 38, dur: 2.4, delay: 0 },
|
||||
{ x: 82, y: 28, dur: 2.0, delay: 0.7 },
|
||||
{ x: 88, y: 33, dur: 2.8, delay: 1.2 },
|
||||
@@ -320,6 +327,7 @@ function ChipPulses() {
|
||||
{ x: 70, y: 48, dur: 3.0, delay: 0.2 },
|
||||
{ x: 95, y: 70, dur: 2.4, delay: 0.9 },
|
||||
];
|
||||
const dots = IS_MOBILE ? allDots.filter((_, i) => i % 2 === 0) : allDots;
|
||||
return (
|
||||
<div className="chip-pulses">
|
||||
{dots.map((d, i) => (
|
||||
@@ -364,9 +372,9 @@ function HeroSection({ bgRef, contentRef }) {
|
||||
className="bg-image"
|
||||
style={{ backgroundImage: "url(assets/exterior-factory.png)" }}
|
||||
/>
|
||||
<SmokeLayer chimneys={chimneys} />
|
||||
<SmokeLayer chimneys={IS_MOBILE ? chimneys.filter((_, i) => i % 2 === 0) : chimneys} />
|
||||
<EmberLayer
|
||||
count={22}
|
||||
count={IS_MOBILE ? 10 : 22}
|
||||
area={{ x: 32, y: 48, w: 65, h: 8 }}
|
||||
/>
|
||||
</div>
|
||||
@@ -508,6 +516,7 @@ function App() {
|
||||
const instSceneRef = useRef(null);
|
||||
|
||||
const [progress, setProgress] = useState(0);
|
||||
const lastDepth = useRef(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.gsap || !window.ScrollTrigger) return;
|
||||
@@ -527,8 +536,18 @@ function App() {
|
||||
trigger: ".scroll-track",
|
||||
start: "top top",
|
||||
end: "bottom bottom",
|
||||
scrub: 0.6,
|
||||
onUpdate: (self) => setProgress(self.progress),
|
||||
// On mobile, scrub:true (no smoothing lag) feels snappier and
|
||||
// skips an extra rAF interpolation per frame.
|
||||
scrub: IS_MOBILE ? true : 0.6,
|
||||
onUpdate: (self) => {
|
||||
// Re-render only when the displayed depth (0..100) changes —
|
||||
// otherwise we re-render the whole HUD on every scroll frame.
|
||||
const depth = Math.round(self.progress * 100);
|
||||
if (depth !== lastDepth.current) {
|
||||
lastDepth.current = depth;
|
||||
setProgress(self.progress);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user