Stress Testing ShadCN Components in React: Performance Under Load

Stress Testing ShadCN Components in React: Performance Under Load

ShadCN UI components are well-designed and accessible. What they're not, by default, is performance-tested for your specific usage. A DataTable with 50 rows behaves differently than one with 50,000 rows. A Combobox with 100 options is fine; with 10,000 options, it depends on how you've configured virtualization.

Stress testing ShadCN components means finding these limits before your users do.

What "Stress Testing" Means for UI Components

Server-side stress testing is about concurrent requests. Frontend component stress testing is about rendering performance under data scale:

  • How does a DataTable perform with 1,000 rows? 10,000? 100,000?
  • How does a Select component perform with 500 options?
  • What happens to a Command (combobox) with a large dataset when the user types quickly?
  • Does an infinite scroll implementation cause memory leaks over time?

The failure modes are different from server-side failures: instead of 503 errors, you get janky scrolling, unresponsive inputs, browser tab crashes, or memory growth that eventually kills the tab.

Performance Profiling Setup

Before writing stress tests, set up performance measurement.

React Profiler API:

import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) => {
  console.log(`${id} rendered in ${actualDuration}ms (${phase})`);
};

function StressTestWrapper({ children }: { children: React.ReactNode }) {
  return (
    <Profiler id="stress-test" onRender={onRender}>
      {children}
    </Profiler>
  );
}

Web Vitals measurement:

import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';

onINP((metric) => {
  if (metric.value > 200) {
    console.warn(`Slow interaction: ${metric.value}ms`);
  }
});

INP (Interaction to Next Paint) is the most relevant metric for component responsiveness — it measures the latency of actual user interactions.

Stress Testing the DataTable Component

ShadCN's DataTable (built on TanStack Table) is a common performance bottleneck.

function generateLargeDataset(rows: number) {
  return Array.from({ length: rows }, (_, i) => ({
    id: i,
    name: `User ${i}`,
    email: `user${i}@example.com`,
    status: i % 3 === 0 ? 'active' : 'inactive',
    created: new Date(Date.now() - i * 86400000).toISOString(),
  }));
}

// Stress test: render with increasing row counts
describe('DataTable stress test', () => {
  const rowCounts = [100, 1000, 5000, 10000];
  
  rowCounts.forEach(count => {
    it(`renders ${count} rows within 1000ms`, async () => {
      const data = generateLargeDataset(count);
      const start = performance.now();
      
      render(<DataTable columns={columns} data={data} />);
      
      await waitFor(() => {
        expect(screen.getAllByRole('row').length).toBeGreaterThan(0);
      });
      
      const duration = performance.now() - start;
      expect(duration).toBeLessThan(1000); // fail if render takes >1s
    });
  });
});

What you'll find: Most DataTable implementations without virtualization will start degrading around 1,000-2,000 rows. The DOM nodes multiply, scrolling gets janky, filtering gets slow.

The fix: Add virtualization with @tanstack/react-virtual:

import { useVirtualizer } from '@tanstack/react-virtual';

// ... inside your table component
const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 35,
  overscan: 10,
});

After adding virtualization, run the stress test again. 10,000 rows should render fast and scroll smoothly.

Stress Testing Select and Combobox

The Command component (used for comboboxes) and Select are common bottlenecks with large option sets.

function generateOptions(count: number) {
  return Array.from({ length: count }, (_, i) => ({
    value: `option-${i}`,
    label: `Option ${i}: ${randomWords()}`,
  }));
}

describe('Combobox with large option sets', () => {
  it('renders 1000 options without visible delay', async () => {
    const options = generateOptions(1000);
    render(<ProductCombobox options={options} />);
    
    const trigger = screen.getByRole('combobox');
    
    const start = performance.now();
    await userEvent.click(trigger);
    
    await waitFor(() => {
      expect(screen.getByRole('listbox')).toBeInTheDocument();
    });
    
    const openDuration = performance.now() - start;
    expect(openDuration).toBeLessThan(200); // should open fast
  });
  
  it('filters 1000 options within 100ms of typing', async () => {
    const options = generateOptions(1000);
    render(<ProductCombobox options={options} />);
    
    await userEvent.click(screen.getByRole('combobox'));
    
    const input = screen.getByRole('searchbox');
    const start = performance.now();
    await userEvent.type(input, 'Option 5');
    const typeDuration = performance.now() - start;
    
    // Typing should be fast even with 1000 options
    expect(typeDuration / 8).toBeLessThan(100); // per character
  });
});

What you'll find: The Command component's built-in fuzzy search with cmdk is fast up to a few hundred items. Beyond that, you need to either virtualize the list or move filtering to the server.

Memory Leak Testing

ShadCN components can accumulate memory over time if used in certain patterns — particularly components with event listeners that don't clean up properly.

describe('Memory leak check', () => {
  it('does not leak memory when mounted and unmounted repeatedly', async () => {
    const initialMemory = (performance as any).memory?.usedJSHeapSize;
    
    for (let i = 0; i < 100; i++) {
      const { unmount } = render(<ComplexDataTable data={largeDataset} />);
      await waitFor(() => screen.getAllByRole('row'));
      unmount();
    }
    
    // Force garbage collection if available (Chrome DevTools)
    if (global.gc) global.gc();
    
    const finalMemory = (performance as any).memory?.usedJSHeapSize;
    const memoryGrowth = finalMemory - initialMemory;
    
    // Allow some growth but flag significant leaks
    expect(memoryGrowth).toBeLessThan(10 * 1024 * 1024); // 10MB
  });
});

Interaction Stress Testing

Beyond rendering performance, test interaction performance — how the component responds to rapid user input.

it('handles rapid consecutive selections without errors', async () => {
  const onSelect = jest.fn();
  render(<DataTable columns={columns} data={largeDataset} onRowSelect={onSelect} />);
  
  const rows = screen.getAllByRole('row').slice(1, 11); // first 10 data rows
  
  // Rapidly click multiple rows
  for (const row of rows) {
    await userEvent.click(row);
  }
  
  // All clicks should be registered
  expect(onSelect).toHaveBeenCalledTimes(10);
  // No duplicate calls or missed events
  const selectedIds = onSelect.mock.calls.map(call => call[0].id);
  expect(new Set(selectedIds).size).toBe(10);
});

Integration With Behavioral Testing

Component-level performance tests verify isolated rendering. Behavioral tests verify the full user experience in a real browser.

After fixing component performance issues, verify in a real browser with HelpMeTest:

Go to https://myapp.com/users (page with large DataTable)
Verify the page loads within 3 seconds
Scroll to the bottom of the table
Verify scrolling is smooth (no timeout)
Click the "Filter" button
Type "admin" in the filter
Verify filtered results appear within 500ms

These behavioral tests run continuously. If a future code change introduces a performance regression — a new dependency that slows the component, a data growth that pushes you past the virtualization threshold — the behavioral test will catch it.

Common ShadCN Performance Fixes

DataTable with many rows: Add virtualization (@tanstack/react-virtual).

Combobox with many options: Add virtualization or server-side filtering with debounced search.

Complex forms that re-render too much: Isolate form fields with React.memo or use useFormContext carefully to prevent full form re-renders on every field change.

Modal/Dialog with heavy content: Render modal content lazily — don't render the full content until the modal is open.

Charts and visualizations inside ShadCN cards: Virtualize or lazy-load charts below the fold.

The Testing Cadence

  1. Before adding new data-heavy features: Write component stress tests first.
  2. When data volumes in production grow: Re-run stress tests with realistic data sizes.
  3. Continuously: Run behavioral tests that verify the user experience stays fast.

Component stress testing is a one-time investment that pays off every time your data grows or your component is reused in a new context.

HelpMeTest provides continuous behavioral testing for web applications — catch performance regressions before users do.

Read more

Start now free