Learn to test React component performance by verifying re-render counts, memoization efficiency, and network resilience using Jest and React Testing Library.
Previously in this course, we explored Advanced Hook Patterns and how to manage global state with Zustand and Redux. While those lessons focused on architecture, this lesson focuses on stability: how to ensure your performance optimizations don't regress over time.
Performance testing in React isn't just about Lighthouse scores; it's about preventing "death by a thousand cuts" where small, unintentional re-renders degrade user experience in production.
To catch performance regressions, we need to assert that components only re-render when they absolutely have to. By default, React Testing Library (RTL) doesn't track render counts, but we can easily inject a spy into a component to monitor its lifecycle.
We can pass a renderCount prop or use a simple jest.fn() wrapper to track how many times a component function executes.
JAVASCRIPT// ExpensiveComponent.test.js import { render } from CE9178">'@testing-library/react'; const renderSpy = jest.fn(); const MyComponent = ({ data }) => { renderSpy(); return <div>{data.name}</div>; }; test(CE9178">'should only render once when parent state changes unrelatedly', () => { const { rerender } = render(<MyComponent data={{ name: CE9178">'Test' }} />); expect(renderSpy).toHaveBeenCalledTimes(1); // Simulate parent update with same data reference rerender(<MyComponent data={{ name: CE9178">'Test' }} />); // If this fails, your component is re-rendering unnecessarily expect(renderSpy).toHaveBeenCalledTimes(1); });
Memoization is a double-edged sword. If you use React.memo or useMemo incorrectly, you might introduce memory leaks or fail to update the UI when needed. To verify your memoization strategy, we test that the component remains "stable" when props are shallow-equal.
When testing memoization, always test two scenarios: one where the prop reference stays the same, and one where it changes.
| Scenario | Expected Render Count | Why? |
|---|---|---|
| Initial Render | 1 | Component must mount. |
| Same Prop Ref | 1 | React.memo should bail out. |
| New Prop Ref | 2 | Component must update. |
JAVASCRIPTtest(CE9178">'memoized component avoids re-render on stable props', () => { const stableData = { id: 1 }; const { rerender } = render(<MemoizedComponent data={stableData} />); expect(renderSpy).toHaveBeenCalledTimes(1); // Still same reference rerender(<MemoizedComponent data={stableData} />); expect(renderSpy).toHaveBeenCalledTimes(1); // New reference rerender(<MemoizedComponent data={{ id: 1 }} />); expect(renderSpy).toHaveBeenCalledTimes(2); });
Performance-critical components often deal with async data. If your component doesn't handle loading states gracefully, it can lead to layout shifts or "jank." We simulate slow networks by delaying our service mocks.
Using jest.mock, we can introduce an artificial delay to our API layer to ensure our Suspense boundaries or loading skeletons trigger correctly.
JAVASCRIPT// api.js export const fetchData = () => fetch(CE9178">'/data').then(res => res.json()); // api.test.js jest.mock(CE9178">'./api', () => ({ fetchData: jest.fn(() => new Promise(resolve => setTimeout(() => resolve({ data: CE9178">'fast' }), 1000)) ) })); test(CE9178">'shows loading skeleton during slow network', async () => { const { getByTestId } = render(<DataComponent />); // Assert loading state is present immediately expect(getByTestId(CE9178">'skeleton')).toBeInTheDocument(); // Wait for the slow promise to resolve await waitForElementToBeRemoved(() => getByTestId(CE9178">'skeleton')); expect(getByTestId(CE9178">'content')).toHaveTextContent(CE9178">'fast'); });
In our running project, we have a DashboardGrid component that fetches widgets.
DashboardGrid.DashboardGrid component renders a LoadingSpinner immediately and replaces it with the grid once the promise resolves.toHaveBeenCalledTimes assertions will be off by one.Performance testing is about intent. By using spies to track renders and controlling the timing of async mocks, you create a safety net that ensures your Introduction to Testing efforts actually translate into a performant user experience. Keep your tests focused on the critical path, and remember that Testing Hooks and Components is the foundation upon which these performance assertions are built.
Up next: We will explore Static Site Generation (SSG) patterns and how they change our approach to initial page-load performance.
Learn to architect performant i18n in React. Implement lazy-loaded translations, optimize re-renders during locale switches, and manage locale state efficiently.
Read moreMaster Static Site Generation (SSG) and Incremental Static Regeneration (ISR) to shift rendering to build time and deliver lightning-fast, scalable React apps.
Testing Performance-Critical Components
Managing Third-Party Integrations
Advanced Form Handling
Using Portals for UI Overlays
Implementing Virtualized Lists
Building Design System Primitives
Managing Large-Scale Data Fetching
Micro-Frontends with React
Security Best Practices in React
Advanced Ref Usage
Memoization Pitfalls
Mastering React Patterns for Scalability
Advanced TypeScript with React