From 8592775485fdb870d551a92f66d7233561dd1a12 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:24:10 +0000 Subject: [PATCH] perf: throttle scroll event listener in Navbar Implemented `requestAnimationFrame` throttling for the scroll event listener in `Navbar` to reduce the frequency of state updates and logic execution. - Wrapped scroll handler in `requestAnimationFrame`. - Added performance test `src/components/layout/Navbar.test.tsx` verifying logic runs once per frame instead of per event. - Verified functional correctness of scroll state updates. - Fixed `useTypingEffect` test environment. --- src/components/layout/Navbar.test.tsx | 91 +++++++++++++++++++++++++++ src/components/layout/Navbar.tsx | 9 ++- src/hooks/useTypingEffect.test.ts | 1 + 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/components/layout/Navbar.test.tsx diff --git a/src/components/layout/Navbar.test.tsx b/src/components/layout/Navbar.test.tsx new file mode 100644 index 0000000..83b0d65 --- /dev/null +++ b/src/components/layout/Navbar.test.tsx @@ -0,0 +1,91 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, fireEvent, screen, cleanup, act } from '@testing-library/react'; +import { Navbar } from './Navbar'; +import styles from './Navbar.module.css'; +import { LanguageProvider } from '../../i18n'; +import { MemoryRouter } from 'react-router-dom'; + +describe('Navbar Performance', () => { + let scrollYGetterSpy: ReturnType; + + beforeEach(() => { + // Mock window.scrollY + scrollYGetterSpy = vi.fn(() => 0); + Object.defineProperty(window, 'scrollY', { + configurable: true, + get: scrollYGetterSpy, + }); + vi.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + it('throttles scroll events using requestAnimationFrame', () => { + render( + + + + + + ); + + const scrollEvents = 50; + + // Simulate 50 scroll events synchronously + for (let i = 0; i < scrollEvents; i++) { + fireEvent.scroll(window); + } + + // Because of throttling, the expensive logic (accessing scrollY) + // should not have run immediately for every event. + expect(scrollYGetterSpy).toHaveBeenCalledTimes(0); + + // Now advance timers to let RAF callback run + vi.runAllTimers(); + + // It should have run ONCE (or very few times) + expect(scrollYGetterSpy).toHaveBeenCalledTimes(1); + }); + + it('correctly updates style when scrolled', () => { + render( + + + + + + ); + + const header = screen.getByRole('banner'); + + // Initial state check + expect(header.className).not.toContain(styles.scrolled); + + // Simulate scroll > 20 + scrollYGetterSpy.mockReturnValue(50); + fireEvent.scroll(window); + + // Run timers wrapped in act to flush React updates + act(() => { + vi.runAllTimers(); + }); + + // Now it should be scrolled + expect(header.className).toContain(styles.scrolled); + + // Scroll back to top + scrollYGetterSpy.mockReturnValue(0); + fireEvent.scroll(window); + + act(() => { + vi.runAllTimers(); + }); + + expect(header.className).not.toContain(styles.scrolled); + }); +}); diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 1f16221..286f0f8 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -23,8 +23,15 @@ export function Navbar() { const activeIndex = navLinks.findIndex((link) => link.path === location.pathname); useEffect(() => { + let ticking = false; const handleScroll = () => { - setIsScrolled(window.scrollY > 20); + if (!ticking) { + window.requestAnimationFrame(() => { + setIsScrolled(window.scrollY > 20); + ticking = false; + }); + ticking = true; + } }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); diff --git a/src/hooks/useTypingEffect.test.ts b/src/hooks/useTypingEffect.test.ts index 1178a37..a29256f 100644 --- a/src/hooks/useTypingEffect.test.ts +++ b/src/hooks/useTypingEffect.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { renderHook, act } from '@testing-library/react'; import { useTypingEffect } from './useTypingEffect'; import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -- 2.49.1