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({