← Back to Articles

Performance Optimization Techniques for React Apps

Code
6 min read

Performance is crucial for web applications. I've spent a lot of time optimizing React apps, and I've learned that it's not just about making things faster—it's about creating a smooth, responsive user experience. React provides many tools for optimization, but knowing when and how to use them is key.

Understanding performance bottlenecks

Before you start optimizing, you need to identify what's slow. I always start with measurement:

  • Chrome DevTools Performance tab: Record and analyze runtime performance
  • Lighthouse: Automated performance audits
  • React DevTools Profiler: Component-level performance analysis
  • Bundle analyzer: Identify large dependencies

Code splitting

Code splitting is one of the most impactful optimizations. Instead of loading everything at once, split your code into chunks that load when needed.

Route-based splitting with React.lazy:

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

Component-based splitting:

import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./components/HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Analytics
      </button>

      {showChart && (
        <Suspense fallback={<div>Loading chart...</div>}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Memoization

Memoization prevents unnecessary re-renders by caching expensive computations.

React.memo for components:

const UserCard = React.memo(({ user, onClick }) => {
  console.log('UserCard rendered for:', user.name);
  return (
    <div onClick={() => onClick(user.id)}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

useMemo for expensive calculations:

function UserList({ users, filter }) {
  const filteredUsers = useMemo(() => {
    console.log('Filtering users...');
    return users.filter(user =>
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

  return (
    <div>
      {filteredUsers.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

useCallback for stable function references:

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // Empty dependency array since it doesn't depend on props/state

  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
}

Virtualization for large lists

When displaying hundreds or thousands of items, virtualization renders only what's visible.

Using react-window:

import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={50}
      width={300}
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </List>
  );
}

Optimizing bundle size

Large bundles slow down initial page loads. Several strategies help:

Tree shaking:

// Only import what you need
import { map, filter } from 'lodash-es'; // Instead of import _ from 'lodash'

Dynamic imports:

// Load heavy libraries only when needed
const handleClick = async () => {
  const { heavyFunction } = await import('./heavy-utils');
  heavyFunction();
};

Bundle analysis:

npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

Image optimization

Images often account for most of a page's weight.

Next.js Image component:

import Image from 'next/image';

export default function OptimizedImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={800}
      height={600}
      priority // For above-the-fold images
      placeholder="blur"
      sizes="(max-width: 768px) 100vw, 50vw"
    />
  );
}

Modern image formats:

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Modern image">
</picture>

State management optimization

Efficient state management prevents unnecessary renders.

Context with useMemo:

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const contextValue = useMemo(() => ({
    theme,
    setTheme,
  }), [theme]);

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Redux optimization:

// Use reselect for computed values
import { createSelector } from 'reselect';

const getVisibleTodos = createSelector(
  [getTodos, getFilter],
  (todos, filter) => {
    switch (filter) {
      case 'SHOW_COMPLETED':
        return todos.filter(todo => todo.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(todo => !todo.completed);
      default:
        return todos;
    }
  }
);

Server-side rendering and hydration

SSR can improve initial page load performance.

Next.js SSR:

// pages/ssr-page.js
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

export default function SSRPage({ data }) {
  return (
    <div>
      <h1>Server-Side Rendered</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Static generation for static content:

// pages/static-page.js
export async function getStaticProps() {
  const data = await fetchStaticData();
  return { props: { data } };
}

export default function StaticPage({ data }) {
  return <div>{/* Static content */}</div>;
}

Monitoring and measurement

Performance optimization is ongoing. Set up monitoring:

Web Vitals:

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);

Real user monitoring (RUM):

// Using a service like Sentry or LogRocket
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: 'your-dsn',
  tracesSampleRate: 1.0,
});

Common performance pitfalls

Avoid these common mistakes:

  1. Creating functions in render: Move them outside or use useCallback
  2. Object/array creation in render: Use useMemo
  3. Unnecessary re-renders: Check dependency arrays
  4. Large bundle sizes: Code split and lazy load
  5. Inefficient selectors: Use reselect for Redux
  6. Missing keys: Always provide keys for lists

Performance budgeting

Set performance budgets and monitor them:

// webpack.config.js
module.exports = {
  performance: {
    hints: 'warning',
    maxAssetSize: 500000,
    maxEntrypointSize: 500000,
  },
};

Continuous optimization

Performance optimization isn't a one-time task:

  • Regular audits: Use Lighthouse regularly
  • Monitor trends: Track performance over time
  • User feedback: Listen to performance complaints
  • A/B testing: Test performance improvements
  • Stay updated: Keep up with React and browser updates

My optimization philosophy

I focus on perceived performance as much as actual performance. Users care about how fast things feel, not just technical metrics. Techniques like skeleton screens, optimistic updates, and progressive loading can make apps feel faster even if the raw numbers aren't perfect.

Start with measurement, identify bottlenecks, and optimize iteratively. The biggest performance gains usually come from the first few optimizations. After that, you're often dealing with diminishing returns.

Remember that performance is a feature. Investing in it early pays dividends in user satisfaction and business metrics. A fast app is a competitive advantage.

About the author

Rafael De Paz

Full Stack Developer

Passionate full-stack developer specializing in building high-quality web applications and responsive sites. Expert in robust data handling, leveraging modern frameworks, cloud technologies, and AI tools to deliver scalable, high-performance solutions that drive user engagement and business growth. I harness AI technologies to accelerate development, testing, and debugging workflows.

Tags:

Share: