From 5d6d938f104492a6096ebc8b2a4ad4dccee1cf74 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Thu, 22 Jan 2026 08:20:09 +0000
Subject: [PATCH] perf: throttle Hero scroll listener with
requestAnimationFrame
- Implements requestAnimationFrame throttling for the scroll event listener in Hero.tsx
- Adds cleanup for the animation frame on component unmount
- Adds unit tests to verify behavior and throttling logic
---
src/components/sections/Hero.test.tsx | 135 ++++++++++++++++++++++++++
src/components/sections/Hero.tsx | 16 ++-
2 files changed, 149 insertions(+), 2 deletions(-)
create mode 100644 src/components/sections/Hero.test.tsx
diff --git a/src/components/sections/Hero.test.tsx b/src/components/sections/Hero.test.tsx
new file mode 100644
index 0000000..540ce68
--- /dev/null
+++ b/src/components/sections/Hero.test.tsx
@@ -0,0 +1,135 @@
+import { render, fireEvent, act, cleanup } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { Hero } from "./Hero";
+import { MemoryRouter } from "react-router-dom";
+
+// Mock dependencies
+vi.mock("../../i18n", () => ({
+ useTranslation: () => ({
+ t: {
+ hero: {
+ company: "Test Company",
+ greeting: "Hello",
+ tagline: "Building the Future",
+ rotatingWords: ["Code", "Design"],
+ cta: "Contact Us",
+ ctaSecondary: "About Us",
+ scroll: "Scroll Down",
+ },
+ },
+ }),
+}));
+
+vi.mock("../../hooks", () => ({
+ useTypingEffect: () => ({
+ text: "Code",
+ }),
+}));
+
+vi.mock("../effects", () => ({
+ GradientBlinds: () =>
,
+}));
+
+// Mock Link from react-router-dom to avoid needing Router context for just Link,
+// OR just use MemoryRouter in the render. MemoryRouter is safer.
+
+describe("Hero Component", () => {
+ beforeEach(() => {
+ // Reset scroll position
+ window.scrollY = 0;
+ vi.clearAllMocks();
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.useRealTimers();
+ });
+
+ it("renders correctly and shows scroll indicator initially", () => {
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText("Test Company")).toBeDefined();
+ expect(getByText("Scroll Down")).toBeDefined();
+ });
+
+ it("hides scroll indicator when scrolled down", () => {
+ const { queryByText } = render(
+
+
+
+ );
+
+ expect(queryByText("Scroll Down")).toBeDefined();
+
+ // Simulate scroll
+ act(() => {
+ window.scrollY = 100;
+ window.dispatchEvent(new Event("scroll"));
+ vi.runAllTimers();
+ });
+
+ expect(queryByText("Scroll Down")).toBeNull();
+ });
+
+ it("shows scroll indicator again when scrolled back up", () => {
+ const { queryByText } = render(
+
+
+
+ );
+
+ // Initial scroll down
+ act(() => {
+ window.scrollY = 100;
+ window.dispatchEvent(new Event("scroll"));
+ vi.runAllTimers();
+ });
+ expect(queryByText("Scroll Down")).toBeNull();
+
+ // Scroll back up
+ act(() => {
+ window.scrollY = 0;
+ window.dispatchEvent(new Event("scroll"));
+ vi.runAllTimers();
+ });
+ expect(queryByText("Scroll Down")).toBeDefined();
+ });
+
+ it("throttles scroll events using requestAnimationFrame", () => {
+ const rafSpy = vi.spyOn(window, "requestAnimationFrame");
+ render(
+
+
+
+ );
+
+ // Trigger multiple scroll events rapidly
+ act(() => {
+ window.scrollY = 10;
+ window.dispatchEvent(new Event("scroll"));
+ window.dispatchEvent(new Event("scroll"));
+ window.dispatchEvent(new Event("scroll"));
+ });
+
+ // Should call rAF only once before timers run (because of ticking flag)
+ expect(rafSpy).toHaveBeenCalledTimes(1);
+
+ // Run timers to reset ticking
+ act(() => {
+ vi.runAllTimers();
+ });
+
+ // Trigger again
+ act(() => {
+ window.dispatchEvent(new Event("scroll"));
+ });
+
+ // Should call rAF again
+ expect(rafSpy).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx
index 9c185a9..f7855f2 100644
--- a/src/components/sections/Hero.tsx
+++ b/src/components/sections/Hero.tsx
@@ -14,12 +14,24 @@ export function Hero() {
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
useEffect(() => {
+ let ticking = false;
+ let rafId: number;
+
const handleScroll = () => {
- setShowScrollIndicator(window.scrollY < 50);
+ if (!ticking) {
+ rafId = window.requestAnimationFrame(() => {
+ setShowScrollIndicator(window.scrollY < 50);
+ ticking = false;
+ });
+ ticking = true;
+ }
};
window.addEventListener("scroll", handleScroll, { passive: true });
- return () => window.removeEventListener("scroll", handleScroll);
+ return () => {
+ window.removeEventListener("scroll", handleScroll);
+ if (rafId) window.cancelAnimationFrame(rafId);
+ };
}, []);
const { text } = useTypingEffect({
--
2.49.1