How to boost React Performance. UseMemo, UseCallback, and Lazy Loading

by Evgenii Studitskikh
4 minutes read

As React applications grow in complexity, performance optimization becomes crucial for maintaining a smooth user experience. In this article, we’ll explore three powerful optimization techniques that every React developer should know:

  1. useMemo – For memoizing computed values
  2. useCallback – For memoizing functions
  3. Lazy Loading – For code splitting and performance

Useful Resources:

Let’s dive deep into each technique and see how they can improve your application’s performance.

useMemo: Optimizing Computed Values

The useMemo hook helps prevent unnecessary recalculations of complex computations. It memoizes the result and only recomputes when dependencies change.

Further Reading:

When to Use useMemo

  • Complex calculations
  • Creating new objects that are used as dependencies in other hooks
  • Preventing re-renders in child components

Example:

import { useMemo } from 'react';

function ProductList({ products, filterText }) {
  // Bad: This filters the list on every render
  const filteredProducts = products.filter(product => 
    product.name.toLowerCase().includes(filterText.toLowerCase())
  );

  // Good: Only recalculates when products or filterText change
  const memoizedFilteredProducts = useMemo(() => {
    return products.filter(product => 
      product.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [products, filterText]);

  return (
    <ul>
      {memoizedFilteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
JavaScript

useCallback: Optimizing Function References

useCallback is used to memoize functions, preventing unnecessary re-renders of child components that receive these functions as props.

Further Reading:

When to Use useCallback

  • When passing functions to optimized child components that rely on reference equality
  • When a function is used as a dependency in other hooks
  • In scenarios where function creation is expensive

Example:

import { useCallback } from 'react';

function ParentComponent() {
  // Bad: New function reference created on every render
  const handleClick = (id) => {
    console.log('Item clicked:', id);
  };

  // Good: Function reference remains stable
  const memoizedHandleClick = useCallback((id) => {
    console.log('Item clicked:', id);
  }, []); // Empty deps array since function doesn't use any external values

  return (
    <ChildComponent onClick={memoizedHandleClick} />
  );
}

// Using React.memo to prevent unnecessary re-renders
const ChildComponent = React.memo(({ onClick }) => {
  console.log('Child component rendered');
  return <button onClick={() => onClick(1)}>Click me</button>;
});
JavaScript

Lazy Loading: Code Splitting Made Easy

Lazy loading allows you to split your code into smaller chunks and load them on demand, improving initial load time.

Further Reading:

When to Use Lazy Loading

  • For large components that aren’t immediately needed
  • Route-based code splitting
  • Feature-based code splitting

Example:

import { lazy, Suspense } from 'react';

// Bad: Importing everything upfront
import HeavyComponent from './HeavyComponent';

// Good: Lazy loading the component
const LazyHeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyHeavyComponent />
      </Suspense>
    </div>
  );
}
JavaScript

Route-Based Example:

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

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  );
}
JavaScript

Best Practices and Common Pitfalls

  1. Don’t Overuse Optimization
   // Don't memoize simple values
   const value = useMemo(() => 1 + 2, []); // Unnecessary!

   // Do memoize complex calculations
   const value = useMemo(() => expensiveCalculation(props.data), [props.data]);
JavaScript
  1. Proper Dependency Arrays
   // Bad: Missing dependencies
   const memoizedValue = useMemo(() => data.filter(item => item.id === id), []);

   // Good: All dependencies included
   const memoizedValue = useMemo(() => data.filter(item => item.id === id), [data, id]);
JavaScript
  1. Combining Techniques
   const MemoizedComponent = React.memo(({ data, onUpdate }) => {
     const processedData = useMemo(() => processData(data), [data]);
     const handleUpdate = useCallback(() => onUpdate(data), [data, onUpdate]);

     return (
       <div onClick={handleUpdate}>
         {processedData.map(item => <span key={item.id}>{item.name}</span>)}
       </div>
     );
   });
JavaScript

Additional Resources

Conclusion

Performance optimization in React requires a balanced approach. While these tools are powerful, they should be used judiciously. Always measure performance before and after optimization to ensure your changes are beneficial. Remember:

  • Use useMemo for expensive calculations and object creation
  • Use useCallback when passing functions to optimized child components
  • Implement lazy loading for code splitting and better initial load times

By applying these techniques appropriately, you can significantly improve the performance of your React application while maintaining clean and maintainable code.

You may also like