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.
This commit is contained in:
91
src/components/layout/Navbar.test.tsx
Normal file
91
src/components/layout/Navbar.test.tsx
Normal file
@@ -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<typeof vi.fn>;
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<LanguageProvider>
|
||||
<Navbar />
|
||||
</LanguageProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<LanguageProvider>
|
||||
<Navbar />
|
||||
</LanguageProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user