React Performance Optimization in the Trenches: Real-world War Stories

"The dashboard was taking 8 seconds to load." This critical feedback from our largest client initiated an intensive three-month optimization journey. Our React application had grown to encompass over 500 components serving millions of daily users. Here's the systematic approach we took to cut load times by 75%.

The Initial Challenge

Understanding the Problem

It began with what seemed like a straightforward feature request: implementing real-time dashboard updates. The initial implementation appeared reasonable on paper:

// Initial implementation with performance issues
function Dashboard() {
  const [data, setData] = useState<DashboardData[]>([])
  const [filters, setFilters] = useState<FilterOptions>({})
  const [sortBy, setSortBy] = useState<SortKey>('date')

  // Inefficient: Expensive calculations on every render
  const filteredData = data
    .filter((item) => matchesFilters(item, filters))
    .sort((a, b) => sortItems(a, b, sortBy))

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com')
    ws.onmessage = (event) => {
      // Problem: Creates new array on every update
      setData((prev) => [...prev, JSON.parse(event.data)])
    }
    return () => ws.close()
  }, [])

  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      <DataGrid data={filteredData} />
    </div>
  )
}

The problems weren't obvious at first. But as our dataset grew, the performance issues became painful.

The Investigation

I fired up React DevTools and what I saw made me spill my coffee:

// What we thought was happening
const renderCycles = {
  dashboard: 1,
  filterPanel: 1,
  dataGrid: 1,
  total: 3,
}

// What was actually happening
const actualRenderCycles = {
  dashboard: 1,
  filterPanel: 50, // 😱 Re-rendering on every data update
  dataGrid: 50, // 😱 Complete re-render of 100+ rows
  dataRows: 5000, // πŸ’€ Each row re-rendering unnecessarily
  total: 5100, // πŸ”₯ Total render cycles
}

The Optimization Strategy

Implementing Performance Improvements

// Optimized implementation with proper memoization
function Dashboard() {
  const [data, setData] = useState<DashboardData[]>([])
  const [filters, setFilters] = useState<FilterOptions>({})
  const [sortBy, setSortBy] = useState<SortKey>('date')

  // Performance improvement: Memoized calculations
  const filteredData = useMemo(() => {
    return data
      .filter((item) => matchesFilters(item, filters))
      .sort((a, b) => sortItems(a, b, sortBy))
  }, [data, filters, sortBy])

  const handleMessage = useCallback((newData: DashboardData) => {
    setData((prev) => {
      const updated = [...prev, newData]
      return updated.slice(-1000) // Prevent memory issues
    })
  }, [])

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com')
    ws.onmessage = (event) => handleMessage(JSON.parse(event.data))
    return () => ws.close()
  }, [handleMessage])

  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      <DataGrid data={filteredData} />
    </div>
  )
}

Then we optimized our grid rendering:

// βœ… Virtualized grid component
function DataGrid({ data }) {
  return (
    <VirtualScroll
      itemCount={data.length}
      height={800}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <Row key={data[index].id} data={data[index]} style={style} />
      )}
    </VirtualScroll>
  )
}

// βœ… Memoized row component
const Row = memo(
  function Row({ data, style }) {
    return (
      <div style={style} className="grid-row">
        <Cell>{data.name}</Cell>
        <Cell>{data.value}</Cell>
        <Cell>{formatDate(data.timestamp)}</Cell>
      </div>
    )
  },
  (prev, next) => {
    // Custom comparison to prevent unnecessary re-renders
    return (
      prev.data.id === next.data.id &&
      prev.data.timestamp === next.data.timestamp
    )
  },
)

But the real breakthrough came when we implemented data windowing:

function useWindowedData(allData, windowSize = 50) {
  const [visibleData, setVisibleData] = useState([])
  const [isNearingEnd, setIsNearingEnd] = useState(false)

  // βœ… Maintain a sliding window of data
  const updateVisibleData = useCallback(
    (startIndex: number) => {
      const endIndex = startIndex + windowSize
      const windowedData = allData.slice(startIndex, endIndex)

      setVisibleData(windowedData)
      setIsNearingEnd(endIndex >= allData.length - 20)
    },
    [allData, windowSize],
  )

  // βœ… Intersection observer for infinite scroll
  const bottomRef = useCallback(
    (node) => {
      if (!node) return

      const observer = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting && isNearingEnd) {
            const nextStartIndex = visibleData.length
            updateVisibleData(nextStartIndex)
          }
        },
        { threshold: 0.5 },
      )

      observer.observe(node)
      return () => observer.disconnect()
    },
    [isNearingEnd, visibleData.length, updateVisibleData],
  )

  return { visibleData, bottomRef }
}

The Custom Hook Arsenal

We built a suite of performance-focused hooks:

function useDebounceCallback<T extends (...args: any[]) => any>(
  callback: T,
  delay: number,
): T {
  const timeoutRef = useRef<NodeJS.Timeout>()

  return useCallback(
    (...args: Parameters<T>) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }

      timeoutRef.current = setTimeout(() => {
        callback(...args)
      }, delay)
    },
    [callback, delay],
  ) as T
}

function useThrottledState<T>(
  initialState: T,
  throttleMs = 16, // ~60fps
): [T, (value: T) => void] {
  const [state, setState] = useState(initialState)
  const lastUpdateRef = useRef(0)

  const throttledSetState = useCallback(
    (value: T) => {
      const now = Date.now()

      if (now - lastUpdateRef.current >= throttleMs) {
        setState(value)
        lastUpdateRef.current = now
      }
    },
    [throttleMs],
  )

  return [state, throttledSetState]
}

Measuring Success

Performance Monitoring Implementation

// Enhanced performance tracking system
interface PerformanceMetric {
  component: string
  duration: number
  type: 'render' | 'calculation'
  timestamp: number
}

const PerformanceMetrics = {
  LONG_TASK_THRESHOLD: 50,

  trackRender(Component: string, duration: number): void {
    if (duration > this.LONG_TASK_THRESHOLD) {
      console.warn(
        `Performance Warning: ${Component} took ${duration}ms to render`,
      )
      this.reportMetric({
        component: Component,
        duration,
        type: 'render',
        timestamp: Date.now(),
      })
    }
  },
}

Results and Future Considerations

Measurable Improvements

// Performance metrics before and after optimization
const performanceMetrics = {
  initialLoadTime: {
    before: 8200, // ms
    after: 2100, // ms
    improvement: '74%',
  },
  memoryUsage: {
    before: 180, // MB
    after: 65, // MB
    improvement: '64%',
  },
  userSatisfaction: {
    before: 65, // %
    after: 94, // %
    improvement: '45%',
  },
}

Looking Forward

These optimizations weren't just about speed – they were about creating a sustainable architecture that could scale with our growth. If you're interested in more performance insights, check out my article on Web Performance Optimization where I discuss broader performance strategies.

Remember, premature optimization is the root of all evil, but deliberate, measured performance improvements based on real metrics and user feedback are the foundation of great user experiences.

P.S. Want to see how this fits into a larger architectural discussion? Take a look at my article on Server Components where I explore another approach to React performance optimization.

Comments