Skip to content

Commit 05b2759

Browse files
Optimize parralax and topbar rendering
1 parent aee52ad commit 05b2759

File tree

2 files changed

+110
-40
lines changed

2 files changed

+110
-40
lines changed

src/components/Common/Parallax.tsx

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,119 @@ import { animated, useSpring } from "@react-spring/web";
66
interface ParallaxProps {
77
children: ReactNode;
88
className?: string;
9-
maxOffset?: string;
9+
maxOffset?: string; // e.g. "8rem"
10+
speed?: number; // e.g. 0.2
11+
thresholdPx?: number; // change threshold to avoid tiny updates (default 0.5px)
1012
}
1113

12-
export default function Parallax({ children, className = "", maxOffset = "8rem" }: ParallaxProps) {
13-
const ref = useRef<HTMLDivElement>(null);
14+
export default function Parallax({
15+
children,
16+
className = "",
17+
maxOffset = "8rem",
18+
speed = 0.2,
19+
thresholdPx = 0.5,
20+
}: ParallaxProps) {
21+
const hostRef = useRef<HTMLDivElement>(null);
22+
1423
const [springs, api] = useSpring(() => ({
1524
y: 0,
16-
config: {
17-
mass: 1,
18-
tension: 280,
19-
friction: 120,
20-
},
25+
config: { mass: 1, tension: 280, friction: 120 },
2126
}));
2227

2328
useEffect(() => {
24-
// Reason: Convert CSS units to pixels
25-
const convertToPixels = (value: string) => {
26-
const temp = document.createElement("div");
27-
temp.style.position = "absolute";
28-
temp.style.height = value;
29-
document.body.appendChild(temp);
30-
const pixels = temp.offsetHeight;
31-
document.body.removeChild(temp);
32-
return pixels;
29+
const el = hostRef.current;
30+
if (!el) return;
31+
32+
// ---- convert CSS unit -> px (once) ----
33+
const toPx = (value: string) => {
34+
// single detached element reused for measurement
35+
const probe = document.createElement("div");
36+
probe.style.position = "absolute";
37+
probe.style.visibility = "hidden";
38+
probe.style.height = value;
39+
document.body.appendChild(probe);
40+
const px = probe.offsetHeight;
41+
probe.remove();
42+
return px;
3343
};
44+
const maxOffsetPx = toPx(maxOffset);
3445

35-
const maxOffsetPx = convertToPixels(maxOffset);
46+
// ---- state we keep outside React render ----
47+
let rafId: number | null = null;
48+
let latestScrollY = window.scrollY;
49+
let lastApplied = -1; // last y we sent to spring
50+
let pageVisible = !document.hidden;
51+
let inViewport = true; // will be refined by IO below
3652

37-
const handleScroll = () => {
38-
if (!ref.current) return;
53+
// Only animate when visible + on screen
54+
const shouldRun = () => pageVisible && inViewport;
3955

40-
const scrollY = window.scrollY;
56+
// Coalesced update (max once per frame)
57+
const update = () => {
58+
rafId = null;
59+
if (!shouldRun()) return;
4160

42-
// Reason: Simple parallax based only on scroll position
43-
const parallaxSpeed = 0.2;
44-
const offset = Math.min(scrollY * parallaxSpeed, maxOffsetPx);
61+
const offset = Math.min(latestScrollY * speed, maxOffsetPx);
62+
if (Math.abs(offset - lastApplied) >= thresholdPx) {
63+
lastApplied = offset;
64+
// fire only if value changed enough
65+
api.start({ y: offset });
66+
}
67+
};
68+
69+
const queueUpdate = () => {
70+
if (rafId == null) {
71+
rafId = requestAnimationFrame(update);
72+
}
73+
};
4574

46-
api.start({ y: offset });
75+
const onScroll = () => {
76+
latestScrollY = window.scrollY;
77+
queueUpdate();
4778
};
4879

49-
window.addEventListener("scroll", handleScroll, { passive: true });
50-
window.addEventListener("resize", handleScroll, { passive: true });
80+
// Pause/resume when tab visibility changes
81+
const onVisibility = () => {
82+
pageVisible = !document.hidden;
83+
if (pageVisible) queueUpdate();
84+
};
85+
86+
// Observe element visibility (viewport)
87+
const io = new IntersectionObserver(
88+
(entries) => {
89+
const entry = entries[0];
90+
inViewport = !!entry && (entry.isIntersecting || entry.intersectionRatio > 0);
91+
if (inViewport) queueUpdate();
92+
},
93+
{ root: null, rootMargin: "0px", threshold: [0, 0.01, 0.1, 1] },
94+
);
95+
io.observe(el);
96+
97+
// Recompute when element size/layout changes (less noisy than window resize)
98+
const ro = new ResizeObserver(() => {
99+
// layout shifts can change the perceived parallax; just recompute
100+
queueUpdate();
101+
});
102+
ro.observe(el);
103+
104+
// Initial run
105+
onScroll();
51106

52-
// Reason: Initial calculation on mount
53-
handleScroll();
107+
// passive listener for scroll
108+
window.addEventListener("scroll", onScroll, { passive: true });
109+
document.addEventListener("visibilitychange", onVisibility);
54110

55111
return () => {
56-
window.removeEventListener("scroll", handleScroll);
57-
window.removeEventListener("resize", handleScroll);
112+
window.removeEventListener("scroll", onScroll);
113+
document.removeEventListener("visibilitychange", onVisibility);
114+
io.disconnect();
115+
ro.disconnect();
116+
if (rafId != null) cancelAnimationFrame(rafId);
58117
};
59-
}, [api, maxOffset]);
118+
}, [api, maxOffset, speed, thresholdPx]);
60119

61120
return (
62-
<div ref={ref} className={`relative ${className}`}>
121+
<div ref={hostRef} className={`relative ${className}`}>
63122
<animated.div
64123
style={{
65124
transform: springs.y.to((y) => `translate3d(0, ${y}px, 0)`),

src/components/Common/TopBar.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,26 @@ export default function TopBar() {
3030
const [showBackground, setShowBackground] = useState(false);
3131

3232
useEffect(() => {
33-
const handleScroll = () => {
34-
const scrollY = window.scrollY;
35-
setShowBackground(scrollY > 0);
36-
};
33+
let last = window.scrollY > 0;
34+
setShowBackground(last);
35+
let ticking = false;
3736

38-
window.addEventListener("scroll", handleScroll);
39-
handleScroll(); // Set initial state
37+
const onScroll = () => {
38+
const y = window.scrollY;
39+
if (ticking) return;
40+
ticking = true;
41+
requestAnimationFrame(() => {
42+
const next = y > 0;
43+
if (next !== last) {
44+
last = next;
45+
setShowBackground(next);
46+
}
47+
ticking = false;
48+
});
49+
};
4050

41-
return () => window.removeEventListener("scroll", handleScroll);
51+
window.addEventListener("scroll", onScroll, { passive: true });
52+
return () => window.removeEventListener("scroll", onScroll);
4253
}, []);
4354

4455
return (

0 commit comments

Comments
 (0)