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);
|
const activeIndex = navLinks.findIndex((link) => link.path === location.pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let ticking = false;
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
setIsScrolled(window.scrollY > 20);
|
setIsScrolled(window.scrollY > 20);
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
import { renderHook, act } from '@testing-library/react';
|
import { renderHook, act } from '@testing-library/react';
|
||||||
import { useTypingEffect } from './useTypingEffect';
|
import { useTypingEffect } from './useTypingEffect';
|
||||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||||
|
|||||||
Reference in New Issue
Block a user