Skip to content

Latest commit

 

History

History
1126 lines (880 loc) · 32.8 KB

File metadata and controls

1126 lines (880 loc) · 32.8 KB

React Performance Optimization

⚠️ This is intended to be a comprehensive advanced self-guided topic outline. You may come back to the various activities over the course of a week. See respective time estimates for each activity below. ⚠️

Prerequisites

JavaScript Fundamentals

React Basics

Web Development Tools

Advanced Concepts (helpful but not required)

Table of Contents

Objectives

  • Understand why performance matters in React applications
  • Learn to identify common performance bottlenecks
  • Master key React performance optimization techniques
  • Apply performance optimization strategies to real-world scenarios
  • Measure and verify performance improvements

Motivation and Real-World Application

  • How poor performance impacts user experience and business metrics
  • Case studies of companies that improved conversion rates through React optimization
  • Performance as a critical skill for React developers in competitive job markets
  • How performance optimization demonstrates senior-level engineering skills

Specific Things to Learn

  • React rendering lifecycle and performance implications
  • Component memoization with React.memo, useMemo, and useCallback
  • Code splitting and lazy loading techniques
  • State management optimization strategies
  • Virtual DOM and reconciliation process
  • Profiling and measuring React performance

Lesson

1️⃣ Understanding React's Rendering Process

// Example showing how React updates the DOM
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

2️⃣ Memoization Techniques

Click to see JavaScript code
// Using React.memo to prevent unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
  // Complex rendering logic here
  return <div>{/* Rendered content */}</div>;
});

// Using useMemo for expensive calculations
function DataProcessor({ items }) {
  const processedData = useMemo(() => {
    return items.map((item) => expensiveOperation(item));
  }, [items]);

  return <div>{/* Render using processedData */}</div>;
}

// Using useCallback for stable function references
function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(
    () => {
      // Handle click logic
    },
    [
      /* dependencies */
    ]
  );

  return <ChildComponent onClick={handleClick} />;
}

3️⃣ Code Splitting and Lazy Loading

Click to see JavaScript code
// Dynamic imports with React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

4️⃣ Optimizing Lists and Large Data Sets

Click to see JavaScript code
// Using virtualization for long lists
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  return (
    <FixedSizeList
      height={500}
      width={300}
      itemCount={items.length}
      itemSize={35}
    >
      {Row}
    </FixedSizeList>
  );
}

Self-Guided Activities

🎭 Activity 1: Interactive Performance Profiling

Estimated Time: 30-45 minutes

  • Setup (5-10 minutes): Creating the CodeSandbox project and copying the initial code
  • Analysis (10-15 minutes): Using React DevTools to profile the application and identify issues
  • Implementation (10-15 minutes): Adding the optimizations (useMemo, useCallback, keys)
  • Verification and Reflection (5-10 minutes): Re-profiling and answering reflection questions

Setup :

  1. Create a new React application using CodeSandbox (https://codesandbox.io/s/new) or your preferred online code editor
    • In CodeSandbox, click the "Open in New Window" button in the top-right corner of the preview
    • This opens your app in a dedicated browser tab where DevTools and React Profiler work more reliably
    • In the new window, press Option + Command + I (Mac) to open DevTools
Copy and paste the following code into your `App.js` file:
import React, { useState } from 'react';
import './styles.css';

function ChildComponent({ onClick }) {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
}

function ExpensiveCalculation({ input }) {
  console.log('ExpensiveCalculation rendered');
  // Expensive calculation without memoization
  const result = Array(10000)
    .fill(0)
    .reduce((acc) => acc + input, 0);
  return <div>Result: {result}</div>;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [input, setInput] = useState(5);

  // This function is recreated on every render
  const handleClick = () => {
    console.log('Button clicked');
  };

  return (
    <div className="App">
      <h1>Performance Optimization Activity</h1>

      <div className="section">
        <h2>Counter</h2>
        <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      </div>

      <div className="section">
        <h2>Expensive Calculation</h2>
        <input
          type="number"
          value={input}
          onChange={(e) => setInput(Number(e.target.value))}
        />
        <ExpensiveCalculation input={input} />
      </div>

      <div className="section">
        <h2>Child Component</h2>
        <ChildComponent onClick={handleClick} />
      </div>

      <div className="section">
        <h2>List Without Keys</h2>
        {Array(100)
          .fill(0)
          .map((_, i) => (
            <div>Item {i}</div> // Missing key prop
          ))}
      </div>
    </div>
  );
}
  1. Install the React DevTools extension if you haven't already:
  2. Navigate to the Components tab in React DevTools. Click on the Profiler tab (if available)
  3. Click the record button (circle) in the Profiler
  4. Interact with the application:
    • Click the counter button 3-5 times
    • Change the input value
    • Scroll through the list
  5. Stop the recording

The Problem: The application has several performance issues:

  • A component that re-renders unnecessarily
  • A list without proper key props
  • An expensive calculation without memoization

Analysis Tasks:

  1. Look at the console logs to see which components are rendering
  2. Identify components with frequent re-renders in the Profiler
  3. Note which component takes the longest to render (look for the darkest colored bars)
  4. Review the code and list at least 3 performance issues you've identified

Optimization Tasks:

  1. Fix the expensive calculation using useMemo:
import React, { useState, useMemo, useCallback } from 'react';
// Add the imports above

// Then replace the ExpensiveCalculation component with:
function ExpensiveCalculation({ input }) {
  console.log('ExpensiveCalculation rendered');

  // Memoized calculation
  const result = useMemo(() => {
    return Array(10000)
      .fill(0)
      .reduce((acc) => acc + input, 0);
  }, [input]);

  return <div>Result: {result}</div>;
}
  1. Fix the function recreation with useCallback:
// Replace the handleClick function with:
const handleClick = useCallback(() => {
  console.log('Button clicked');
}, []);
  1. Add proper keys to the list:
// Replace the list rendering with:
<div className="section">
  <h2>List With Keys</h2>
  {Array(100)
    .fill(0)
    .map((_, i) => (
      <div key={i}>Item {i}</div>
    ))}
</div>
  1. Memoize the ChildComponent:
// Replace the ChildComponent with:
const ChildComponent = React.memo(function ChildComponent({ onClick }) {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

Verification:

  1. Record another profiling session in React DevTools
  2. Perform the same interactions as before
  3. Compare the console logs and profile with your observations from the first recording
  4. You should observe:
    • Fewer component renders in the console logs
    • Shorter render durations in the Profiler
    • The ExpensiveCalculation component only updates when the input changes
If the Profiler is still unavailable, modify the code to use console logs:
// Add this to the top of your App.js file
const useRenderCounter = (componentName) => {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered: ${renderCount.current} times`);
  });

  return renderCount.current;
};

// Then modify each component to use it:
function ExpensiveCalculation({ input }) {
  const renders = useRenderCounter('ExpensiveCalculation');
  // Rest of component...
}

function ChildComponent({ onClick }) {
  const renders = useRenderCounter('ChildComponent');
  // Rest of component...
}

function App() {
  const renders = useRenderCounter('App');
  // Rest of component...
}

Reflection Questions:

  1. Which optimization had the biggest impact on performance?
  2. Why did adding keys to the list items matter?
  3. When would you use useMemo vs. useCallback?

🛒 Activity 2: Optimizing a Shopping Cart Component

Estimated Time: 45-60 minutes

  • Setup (5-10 minutes): Creating the project and copying the initial code
  • Analysis (10-15 minutes): Reviewing the code and identifying performance issues
  • Implementation (20-25 minutes): Implementing all five optimization tasks
  • Verification and Reflection (10-15 minutes): Testing the optimized component and answering reflection questions

Setup:

  1. Create a new React application using CodeSandbox or your preferred online code editor
  2. Copy and paste the following code into your App.js file:
Click to view the code
import React, { useState } from 'react';
import './styles.css';

// Sample data
const sampleProducts = Array(20)
  .fill(0)
  .map((_, i) => ({
    id: i,
    name: `Product ${i}`,
    description: `This is product number ${i}`,
    price: Math.floor(Math.random() * 100) + 1
  }));

const sampleUser = {
  id: 1,
  name: 'Jane Doe',
  locale: 'en-US',
  currency: 'USD'
};

export default function App() {
  return (
    <div className="App">
      <h1>Shopping Cart Performance Activity</h1>
      <ShoppingCart products={sampleProducts} user={sampleUser} />
    </div>
  );
}

function ShoppingCart({ products, user }) {
  const [cart, setCart] = useState([]);

  // Calculate total price
  let totalPrice = 0;
  for (const item of cart) {
    const product = products.find((p) => p.id === item.productId);
    totalPrice += product.price * item.quantity;
  }

  // Format currency based on user locale
  const formattedPrice = new Intl.NumberFormat(user.locale, {
    style: 'currency',
    currency: user.currency
  }).format(totalPrice);

  // Add to cart function
  function addToCart(productId) {
    const existingItem = cart.find((item) => item.productId === productId);

    if (existingItem) {
      setCart(
        cart.map((item) =>
          item.productId === productId
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      );
    } else {
      setCart([...cart, { productId, quantity: 1 }]);
    }
  }

  return (
    <div className="shopping-cart">
      <h2>Shopping Cart ({cart.length} items)</h2>
      <div>Total: {formattedPrice}</div>

      <div className="product-list">
        <h3>Products</h3>
        {products.map((product) => (
          <div className="product-card">
            <h4>{product.name}</h4>
            <p>{product.description}</p>
            <p className="price">
              {new Intl.NumberFormat(user.locale, {
                style: 'currency',
                currency: user.currency
              }).format(product.price)}
            </p>
            <button onClick={() => addToCart(product.id)}>Add to Cart</button>
          </div>
        ))}
      </div>

      <div className="cart-items">
        <h3>Cart Items</h3>
        {cart.map((item) => {
          const product = products.find((p) => p.id === item.productId);
          return (
            <div className="cart-item">
              <span>
                {product.name} x {item.quantity}
              </span>
              <span>
                {new Intl.NumberFormat(user.locale, {
                  style: 'currency',
                  currency: user.currency
                }).format(product.price * item.quantity)}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

The Problem: The shopping cart component has several performance issues:

  • Missing keys in both product and cart lists
  • Repeated currency formatting logic
  • Expensive product lookup in multiple places
  • Recreated addToCart function on every render
  • Unnecessary recalculations when cart hasn't changed

Step-by-Step Instructions:

  1. Add the necessary imports at the top of your file:
import React, { useState, useMemo, useCallback, memo } from 'react';
  1. Implement the following optimizations via tasks 1 - 5:
Task 1: Add keys to lists
// Replace the product list rendering with:
{
  products.map((product) => (
    <div key={product.id} className="product-card">
      <h4>{product.name}</h4>
      <p>{product.description}</p>
      <p className="price">
        {new Intl.NumberFormat(user.locale, {
          style: 'currency',
          currency: user.currency
        }).format(product.price)}
      </p>
      <button onClick={() => addToCart(product.id)}>Add to Cart</button>
    </div>
  ));
}

// Replace the cart items rendering with:
{
  cart.map((item) => {
    const product = products.find((p) => p.id === item.productId);
    return (
      <div key={item.productId} className="cart-item">
        <span>
          {product.name} x {item.quantity}
        </span>
        <span>
          {new Intl.NumberFormat(user.locale, {
            style: 'currency',
            currency: user.currency
          }).format(product.price * item.quantity)}
        </span>
      </div>
    );
  });
}
Task 2: Memoize the total price calculation
// Replace the total price calculation with:
const totalPrice = useMemo(() => {
  let total = 0;
  for (const item of cart) {
    const product = products.find((p) => p.id === item.productId);
    total += product.price * item.quantity;
  }
  return total;
}, [cart, products]);
Task 3: Create a reusable formatter function
// Add this inside the ShoppingCart component:
const formatCurrency = useCallback((amount) => {
  return new Intl.NumberFormat(user.locale, {
    style: 'currency',
    currency: user.currency
  }).format(amount);
}, [user.locale, user.currency]);

// Then replace all instances of the formatting code with:
// For total price:
<div>Total: {formatCurrency(totalPrice)}</div>

// For product price:
<p className="price">{formatCurrency(product.price)}</p>

// For cart item price:
<span>{formatCurrency(product.price * item.quantity)}</span>
Task 4: Memoize the addToCart function
// Replace the addToCart function with:
const addToCart = useCallback((productId) => {
  setCart((prevCart) => {
    const existingItem = prevCart.find((item) => item.productId === productId);

    if (existingItem) {
      return prevCart.map((item) =>
        item.productId === productId
          ? { ...item, quantity: item.quantity + 1 }
          : item
      );
    } else {
      return [...prevCart, { productId, quantity: 1 }];
    }
  });
}, []);
Task 5: Create a memoized CartItem component
// Add this before the ShoppingCart component:
const CartItem = memo(function CartItem({ item, product, formatCurrency }) {
  return (
    <div className="cart-item">
      <span>
        {product.name} x {item.quantity}
      </span>
      <span>{formatCurrency(product.price * item.quantity)}</span>
    </div>
  );
});

// Then replace the cart items rendering with:
<div className="cart-items">
  <h3>Cart Items</h3>
  {cart.map((item) => {
    const product = products.find((p) => p.id === item.productId);
    return (
      <CartItem
        key={item.productId}
        item={item}
        product={product}
        formatCurrency={formatCurrency}
      />
    );
  })}
</div>;

Verification:

  1. Add console.log statements at the beginning of the ShoppingCart and CartItem components
  2. Interact with the application by adding products to the cart
  3. Observe the console logs to see how often components re-render
  4. Try adding the same product multiple times and verify that only the relevant components update

Reflection Questions:

  1. How did extracting the CartItem component improve performance?
  2. Why is it important to use the function form of setState (setCart(prevCart => ...)) in the addToCart function?
  3. What dependencies would you need to add to useCallback if the addToCart function needed access to other variables?

📉 Activity 3: Performance Quiz with Code Analysis

For this activity, analyze each pair of code snippets and determine which one is more performant. Write down your answers and explanations.

Estimated Time: 20-30 minutes

  • Reading and analyzing each code snippet pair (3-5 minutes per question)
  • Writing answers and explanations (2-3 minutes per question)
  • Reviewing the provided answers and explanations (5-10 minutes)

Question 1: Which list implementation is more performant?

// Option A
function ListA({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li>{item.name}</li>
      ))}
    </ul>
  );
}

// Option B
function ListB({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Question 2: Which component will prevent unnecessary re-renders?

// Option A
function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Option B
const UserProfile = React.memo(function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
});

Question 3: Which approach to event handling is better for performance?

// Option A
function ButtonList({ count }) {
  return (
    <div>
      {Array(count)
        .fill(0)
        .map((_, i) => (
          <button key={i} onClick={() => console.log(`Button ${i} clicked`)}>
            Button {i}
          </button>
        ))}
    </div>
  );
}

// Option B
function ButtonList({ count }) {
  const handleClick = useCallback((i) => {
    console.log(`Button ${i} clicked`);
  }, []);

  return (
    <div>
      {Array(count)
        .fill(0)
        .map((_, i) => (
          <button key={i} onClick={() => handleClick(i)}>
            Button {i}
          </button>
        ))}
    </div>
  );
}

Question 4: Which state update is more efficient?

// Option A
function Counter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

// Option B
function Counter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

Question 5: Which approach to data fetching is better for performance?

Click to view the code
// Option A
function UserData({ userId }) {
  const [user, setUser] = useState(null);

useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);

if (!user) return <div>Loading...</div>;

return (

<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

// Option B
function UserData({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
let isMounted = true;

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) {
          setUser(data);
        }
      });

    return () => {
      isMounted = false;
    };

}, [userId]);

if (!user) return <div>Loading...</div>;

return (

<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Answers and Explanations:
  1. Option B is more performant. Adding keys helps React identify which items have changed, been added, or been removed, allowing it to update only the necessary DOM elements instead of re-rendering the entire list.
  2. Option B is more performant. React.memo creates a memoized version of the component that only re-renders if its props change. Without memoization, the component will re-render whenever its parent re-renders, even if the props are the same.
  3. Both options have issues. Option B is slightly better because it doesn't create a new function for each button on every render, but it still creates a new inline function for each button. A better solution would be:
function ButtonList({ count }) {
  const handleClick = useCallback((i) => {
    console.log(`Button ${i} clicked`);
  }, []);

  return (
    <div>
      {Array(count)
        .fill(0)
        .map((_, i) => (
          <button key={i} onClick={() => handleClick(i)}>
            Button {i}
          </button>
        ))}
    </div>
  );
}
  1. Option B is more efficient. When using the function form of setState, React guarantees that the state updates will be applied in sequence. In Option A, all three setCount calls use the same value of count, so the counter only increases by 1. In Option B, each update builds on the previous one, so the counter increases by 3.
  2. Option B is better for performance. It prevents memory leaks by cleaning up the effect when the component unmounts. If the component unmounts before the fetch completes, Option A would try to update state on an unmounted component, which can cause memory leaks and errors.

🛍️ Activity 4: E-commerce Performance Optimization Strategy

Scenario: You're working on an e-commerce product listing page with 100+ products that is experiencing performance issues. Users report lag when filtering and sorting products.

Estimated Time: 60-90 minutes

  • Understanding the scenario (5-10 minutes)
  • Analyzing each area and developing strategies (30-45 minutes):
  • State Management (5-10 minutes)
  • Component Structure (5-10 minutes)
  • Memoization Strategy (5-10 minutes)
  • List Virtualization (5-10 minutes)
  • Code Splitting (5-10 minutes)
  • Performance Measurement (5-10 minutes)
  • Optional Implementation Exercise (25-35 minutes)

Component Structure:

App
├── Header
├── FilterPanel
│   ├── CategoryFilter
│   ├── PriceRangeFilter
│   └── SearchFilter
├── SortControls
├── ProductGrid
│   └── ProductCard (x100+)
└── Footer

Task: Develop a comprehensive performance optimization strategy for this application. For each of the following areas, identify potential issues and propose specific solutions:

1. State Management Optimization

Potential Issues:

  • Global state causing unnecessary re-renders
  • Filter changes triggering re-renders of all components
  • Inefficient state updates

Optimization Strategies:

  • Move filter state to a context provider with useMemo for derived state
  • Split contexts to minimize re-renders (e.g., separate contexts for filters and products)
  • Use useReducer for complex state logic
  • Implement debouncing for search input

2. Component Structure Improvements

Potential Issues:

  • Prop drilling causing cascading re-renders
  • Large component trees with unnecessary re-renders
  • Inefficient component composition

Optimization Strategies:

  • Extract smaller, focused components
  • Use React.memo for pure components
  • Implement shouldComponentUpdate or React.memo with custom comparison functions
  • Create specialized components for expensive operations

3. Memoization Strategy

Potential Issues:

  • Expensive calculations being repeated on every render
  • Function references changing on every render
  • Unnecessary re-renders of memoized components due to object literals

Optimization Strategies:

  • Use useMemo for expensive filter and sort operations
  • Apply useCallback for event handlers and callbacks
  • Ensure proper dependency arrays for hooks
  • Use stable references for objects passed as props

4. List Virtualization Plan

Potential Issues:

  • Rendering all 100+ products at once
  • DOM size becoming too large
  • Scroll performance issues

Optimization Strategies:

  • Implement virtualization with react-window or react-virtualized
  • Only render items in the viewport plus a small buffer
  • Add pagination or infinite scrolling
  • Implement lazy loading for product images

5. Code Splitting Opportunities

Potential Issues:

  • Large initial bundle size
  • Slow initial load time
  • Loading unnecessary code

Optimization Strategies:

  • Split code by route using React.lazy and Suspense
  • Lazy load heavy components like filters or sorting mechanisms
  • Use dynamic imports for rarely used features
  • Implement progressive loading of features

6. Performance Measurement Approach

How to Measure Success:

  • Use React DevTools Profiler to measure render times before and after
  • Implement Web Vitals monitoring (LCP, FID, CLS)
  • Set up user-centric metrics (time to interactive, time to first meaningful paint)
  • A/B test optimizations with real users
  • Use Lighthouse scores as a benchmark

Implementation Exercise:

To practice these concepts, create a simplified version of the product listing page with the following optimizations:

  1. Implement virtualization for the product grid
  2. Add memoization for expensive operations
  3. Use proper component structure to minimize re-renders
  4. Implement efficient state management for filters

This exercise will help you apply the optimization strategies to a real-world scenario and see the performance improvements firsthand.

Common Mistakes / Misconceptions

Misconception: "Premature optimization is always good"

  • Why measuring first is critical
  • When optimization efforts provide diminishing returns
  • How to prioritize optimization efforts for maximum impact

Misconception: "React.memo will always improve performance"

  • When memoization can actually hurt performance
  • The cost of prop comparison vs. re-rendering
  • Guidelines for effective use of memoization

Misconception: "Virtual DOM is automatically fast"

  • Understanding that React still has to do work
  • How component structure affects reconciliation
  • Strategies to help React's diffing algorithm work efficiently

Misconception: "Bundle size doesn't matter with fast internet"

  • The impact of JavaScript parsing and execution time
  • How bundle size affects mobile users and those on slower connections
  • Techniques to monitor and reduce bundle size

Misconception: "State management libraries solve all performance issues"

  • When state management can create performance problems
  • Strategies for optimizing Redux, MobX, and other libraries
  • Local state vs. global state performance considerations

Resources and Next Steps

Performance Measurement Tools

Further Learning

Practice Projects

  • Optimize a data grid with 1000+ rows
  • Improve performance in a complex form application
  • Optimize a dashboard with multiple data visualizations
  • Build a virtualized infinite-scroll social media feed with lazy-loaded images
  • Create a real-time collaborative editor with optimized rendering
  • Implement a performance-optimized e-commerce product configurator with many options
  • Build a map visualization with thousands of interactive data points
  • Develop a photo gallery with advanced filtering that maintains smooth performance
  • Create a drag-and-drop kanban board that handles hundreds of cards efficiently
  • Implement a code editor with syntax highlighting that performs well with large files