Deep Dive 04 - Performance Optimization

Deep Dive 04 - Performance Optimization

Hey everyone! Welcome back to Namaste React Deep Dives!

This is the final Deep Dive — and one of the most important! Performance is what separates a good developer from a GREAT developer. A slow app means users leave. A fast app means users stay!

I am super excited because today we will learn how to find performance problems and fix them — exactly how senior engineers do it in real companies! Trust me, these skills will give you a massive edge in interviews!

What we will cover:

  • How to find performance problems with React Profiler
  • Why components re-render (the complete list!)
  • React.memo — when it helps and common mistakes
  • useMemo and useCallback — the honest guide
  • The children-as-props trick (most developers don't know this!)
  • Virtualization — rendering 10,000 items smoothly
  • Lazy loading images
  • Advanced code splitting
  • Web Vitals — LCP, INP, CLS explained simply
  • Interview Questions

1. Find Problems First — The React Profiler

This is the most important rule of performance:

MEASURE FIRST. OPTIMIZE SECOND. Never guess!

Think of it like a doctor:

Bad doctor: "You look sick. Let me give you every medicine we have!"
Good doctor: "Let me run some tests first and find the actual problem."

Bad developer: "My app is slow. Let me add useMemo everywhere!"
Good developer: "Let me profile first and find WHERE it's slow."

How to use the React DevTools Profiler:

Steps to profile your app:
============================
1. Open Chrome DevTools
2. Go to the "Profiler" tab in React DevTools
3. Click the "Record" button (circle icon)
4. Interact with your app (click, scroll, type)
5. Click "Stop"
6. You'll see a "flame chart" — bars showing each component render

Reading the flame chart:
  Yellow bar  = slow render (investigate this!)
  Green bar   = fast render (ok!)
  Gray bar    = component did NOT re-render (good — memoization working!)

Click any bar to see:
  - How long it took to render
  - WHY it re-rendered ("parent re-rendered", "useState changed", etc.)

You can also add a Profiler component directly in your code:

import { Profiler } from "react";

// Wrap the part you want to measure:
function App() {
    return (
        <Profiler
            id="RestaurantList"
            onRender={(id, phase, actualDuration) => {
                console.log(`${id} took ${actualDuration}ms to render (${phase})`);
            }}
        >
            <RestaurantList />
        </Profiler>
    );
}

// actualDuration = how long it actually took
// If it's high → you have a performance problem!
// Remove the Profiler in production — it adds overhead.

2. Why Components Re-Render

This is the root of most React performance problems! Let me explain all the causes:

CAUSE 1: The component's own state changed
==========================================
const [count, setCount] = useState(0);

// Calling setCount() → component re-renders. Makes sense!


CAUSE 2: Parent re-rendered (this surprises most people!)
==========================================================
function Parent() {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={() => setCount(c => c + 1)}>{count}</button>
            <Child />  ← re-renders on EVERY button click!
        </>
    );
}

function Child() {
    console.log("Child rendered!"); // prints on every parent click!
    return <p>I am the child</p>;
}

// "But we're not passing any props to Child!"
// Doesn't matter. React re-renders ALL children by default!
// This is cause #1 of unexpected re-renders in React.


CAUSE 3: Context value changed
================================
const ThemeContext = createContext("light");

function MyComponent() {
    const theme = useContext(ThemeContext);
    // Re-renders every time ThemeContext value changes!
    return <div className={theme}>...</div>;
}


CAUSE 4: Redux/Zustand selected state changed
==============================================
const cartCount = useSelector(state => state.cart.items.length);
// Re-renders every time cart.items.length changes.
Things that do NOT cause re-renders:
=====================================
❌ Changing a ref (.current) — refs are not reactive
❌ Mutating an object directly — React doesn't track mutations
❌ Module-level variables — outside React's awareness

3. React.memo — Skip Re-Renders When Props Haven't Changed

In simple words:

React.memo = "Only re-render this component if its props actually changed."

// Without React.memo:
function RestaurantCard({ restaurant }) {
    console.log("RestaurantCard rendered!"); // runs every time parent renders!
    return <div>{restaurant.name}</div>;
}


// With React.memo:
const RestaurantCard = React.memo(function RestaurantCard({ restaurant }) {
    console.log("RestaurantCard rendered!"); // only runs if 'restaurant' prop changed!
    return <div>{restaurant.name}</div>;
});

// Now if the parent re-renders but passes the SAME restaurant object,
// React SKIPS re-rendering RestaurantCard! 🎉

Common Mistake 1 — Object props always look "different":

const MemoCard = React.memo(RestaurantCard);

// ❌ This BREAKS memo — new object created on every render!
function Parent() {
    return <MemoCard style={{ color: "red" }} />;
    // { color: "red" } on render 1 and { color: "red" } on render 2
    // are TWO DIFFERENT objects in JavaScript!
    // React.memo sees them as different → always re-renders!
}

// ✅ Fix — move the object outside the component:
const cardStyle = { color: "red" }; // created ONCE, never changes

function Parent() {
    return <MemoCard style={cardStyle} />; // same reference every time!
}

Common Mistake 2 — Inline functions always look "different":

// ❌ This BREAKS memo — new function created on every render!
function Parent() {
    return <MemoButton onClick={() => doSomething()} />;
    // New function every render → React.memo sees new prop → re-renders!
}

// ✅ Fix — use useCallback to keep the same function reference:
function Parent() {
    const handleClick = useCallback(() => doSomething(), []);
    return <MemoButton onClick={handleClick} />; // same reference!
}

Common Mistake 3 — Using memo on EVERYTHING:

// React.memo has a cost — it compares props on every render.
// For simple, cheap components the comparison costs MORE than just re-rendering!

// ❌ Don't do this for simple components:
const Title = React.memo(({ text }) => <h1>{text}</h1>);

// ✅ Only use memo on EXPENSIVE components:
// - Components that take 50ms+ to render
// - Components that render a large list of items
// - Components that re-render very frequently with the SAME props

4. useMemo and useCallback — The Honest Guide

The most common question from developers: "Should I wrap everything in useMemo/useCallback?"

Answer: NO! Here is exactly when to use them and when not to.

useMemo — cache an expensive calculation:

// ✅ USE useMemo when the calculation is expensive:
const sortedRestaurants = useMemo(() => {
    return [...restaurants].sort((a, b) => b.avgRating - a.avgRating);
}, [restaurants]);

// Sorting 1000 items is expensive (50ms+) → worth caching!
// Only recalculates when `restaurants` array changes.


// ❌ DON'T USE useMemo for simple calculations:
const doubled = useMemo(() => count * 2, [count]);
// count * 2 takes 0.001ms! useMemo itself costs MORE than this!
// Just write: const doubled = count * 2;


// ✅ USE useMemo when you need a stable reference for React.memo:
const filters = useMemo(() => ({ category, minRating }), [category, minRating]);
// Now 'filters' is the same object reference if category and minRating haven't changed.
// So a React.memo'd component won't re-render because of 'filters'!

useCallback — cache a function reference:

// Think of it like this:
// useMemo    → remembers a VALUE
// useCallback → remembers a FUNCTION

// ✅ USE useCallback when passing a function to a React.memo'd component:
const handleAddToCart = useCallback((item) => {
    dispatch(addItem(item));
}, [dispatch]);

// Now MemoizedCard gets the SAME function reference every render.
// React.memo sees "onClick didn't change" → skips re-render!
return <MemoizedCard onAdd={handleAddToCart} />;


// ❌ DON'T USE useCallback when the child is NOT memoized:
function Parent() {
    const handleClick = useCallback(() => doSomething(), []);
    return <div onClick={handleClick}>Click me</div>;
    // <div> is not a React.memo component!
    // A stable function reference doesn't stop <div> from re-rendering.
    // useCallback here does NOTHING useful!
}
Quick Summary:
==============

useMemo:
  USE when: expensive calculation (sorting big arrays, heavy math)
  USE when: need stable reference to pass to React.memo'd component
  SKIP when: simple calculation like count * 2 or string.toUpperCase()

useCallback:
  USE when: passing function as prop to React.memo'd child component
  SKIP when: the child component is NOT wrapped in React.memo
  SKIP when: the function is only used inside the same component

5. The Children-as-Props Trick

This is the most underused performance trick! Most developers don't know about it!

The Problem:

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

    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
            <SlowComponent />  ← re-renders on EVERY count change! 😩
        </div>
    );
}

// SlowComponent has nothing to do with count, but it still re-renders!
// Solution 1: React.memo — works but requires memoization.
// Solution 2: Children as props — elegant, NO memoization needed!

The Solution — Children as Props:

// Step 1: Extract the stateful part into its own component
// and accept children as a prop:

function Counter({ children }) {
    const [count, setCount] = useState(0);

    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
            {children}  ← comes from OUTSIDE — Counter doesn't create it!
        </div>
    );
}

// Step 2: Pass SlowComponent as children from App:
function App() {
    return (
        <Counter>
            <SlowComponent />  ← created in App's scope
        </Counter>
    );
}

Why does this work?

When Counter re-renders (because count changed):
  - Counter's children prop is the <SlowComponent /> JSX
  - That JSX was created in App — which did NOT re-render!
  - So children prop reference is exactly the same as before
  - React sees "children didn't change" → skips SlowComponent! ✅

Think of it like:
  Counter is a picture frame.
  The painting inside (SlowComponent) was made by App.
  Counter can change its frame color (count state) all it wants.
  The painting never changes — it stays the same!

6. Virtualization — Rendering 10,000 Items Smoothly

Think of it like a window in a building:

Without virtualization:
========================
You're in a 100-floor building and you can see ALL 100 floors at once.
The building needs to build all 100 floors at once — very heavy!

With virtualization (react-window):
=====================================
You look through a window — you only see floors 3, 4, and 5.
As you scroll up, you see 4, 5, 6. Floor 3 is "recycled" to become floor 6.

The building only ever builds 3 floors! Much lighter! 🚀
The Problem:
=============
function HugeList({ items }) {
    return (
        <ul>
            {items.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
    );
}

// If items has 10,000 entries → 10,000 DOM nodes!
// Browser must lay out and paint ALL 10,000 nodes at once.
// Initial render: 2+ seconds 🐌
// Scrolling: janky and laggy 😖
The Solution — react-window:
==============================
npm install react-window

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

function VirtualizedList({ items }) {
    // This function renders ONE row:
    const Row = ({ index, style }) => (
        <li style={style}>  {/* style has position: absolute + top value */}
            {items[index].name}
        </li>
    );

    return (
        <List
            height={600}            // visible area height in pixels
            itemCount={items.length} // total number of items
            itemSize={50}           // height of each item in pixels
            width="100%"
        >
            {Row}
        </List>
    );
}

// Result: 10,000 items but only ~12 DOM nodes exist at any time!
// As user scrolls → react-window adds new nodes, removes old ones.
// Buttery smooth scrolling, instant render! 🚀


// When to use:
// 50 items  → probably fine without virtualization
// 100 items → consider it
// 500 items → should virtualize
// 1000+    → must virtualize

7. Lazy Image Loading

Images are often the biggest performance drain on a page. Loading ALL images upfront — including ones the user hasn't even scrolled to — is wasteful!

In simple words:

Lazy loading = "Don't load this image until the user is about to see it!"

// The EASIEST way — native browser lazy loading:
<img src="restaurant.webp" loading="lazy" alt="Restaurant" />

// Just add loading="lazy" and the browser handles it!
// Image only downloads when it's near the viewport.
// Works in all modern browsers. No libraries needed!
// Custom lazy loading with Intersection Observer (more control):
function LazyImage({ src, alt }) {
    const [isVisible, setIsVisible] = useState(false);
    const imgRef = useRef();

    useEffect(() => {
        const observer = new IntersectionObserver(([entry]) => {
            if (entry.isIntersecting) {
                setIsVisible(true);    // image is near viewport!
                observer.disconnect(); // stop watching after we see it
            }
        });

        observer.observe(imgRef.current); // watch this element
        return () => observer.disconnect(); // cleanup
    }, []);

    return (
        <div ref={imgRef}>
            {isVisible
                ? <img src={src} alt={alt} />  // load the real image
                : <div style={{ background: "#eee", height: 200 }} />  // placeholder
            }
        </div>
    );
}

// Usage:
<LazyImage src="biryani.webp" alt="Biryani Blues Restaurant" />

8. Advanced Code Splitting

We covered basic code splitting in Chapter 14. Here are the advanced patterns!

PATTERN 1: Split heavy components (not just routes!)
====================================================
// Don't load heavy libraries until they're needed!

// A PDF viewer is huge — why load it on every page?
const PDFViewer = lazy(() => import("./PDFViewer"));

// It only downloads when the user actually needs to view a PDF!
function ReportPage() {
    const [showPDF, setShowPDF] = useState(false);

    return (
        <div>
            <button onClick={() => setShowPDF(true)}>View PDF</button>
            {showPDF && (
                <Suspense fallback={<p>Loading PDF viewer...</p>}>
                    <PDFViewer />
                </Suspense>
            )}
        </div>
    );
}


PATTERN 2: Prefetch on hover (feels INSTANT to users!)
=======================================================
// Start downloading the chunk BEFORE the user clicks!

function NavLink({ to, label, loader }) {
    return (
        <a
            href={to}
            onMouseEnter={loader}  // user hovers → start downloading!
        >
            {label}
        </a>
    );
}

// Usage:
<NavLink
    to="/cart"
    label="Cart"
    loader={() => import("./Cart")}  // starts downloading on hover!
/>

// User moves mouse toward "Cart" → chunk downloads.
// By the time they click → chunk is already there!
// Navigation feels instant! ⚡


PATTERN 3: Adapt to slow networks
==================================
const connection = navigator.connection;
const isSlowNetwork = connection?.effectiveType === "2g";

// Show a simple version on slow networks:
const Charts = isSlowNetwork
    ? () => <p>Charts not available on slow connection</p>
    : lazy(() => import("./HeavyCharts"));

9. Web Vitals — LCP, INP, CLS Explained Simply

Web Vitals are Google's way of measuring how good a user's experience is. They directly affect your Google Search ranking!

The 3 Core Web Vitals:
=======================

LCP (Largest Contentful Paint):
  "How long does the main content take to appear?"
  Think: When does the hero image or main heading show up?
  Target: Under 2.5 seconds ✅

INP (Interaction to Next Paint):
  "How fast does the page respond when you click or type?"
  Think: Click a button → how long until something changes?
  Target: Under 200ms ✅

CLS (Cumulative Layout Shift):
  "Does the page jump around while loading?"
  Think: You're about to click a button, it suddenly moves, you click something else!
  Target: Under 0.1 ✅

How to improve LCP (make main content load faster):

1. Preload your hero image:
============================
<!-- Add this in your <head> -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />

Without preload: browser finds image only after parsing HTML → late download
With preload: browser starts downloading image immediately with HTML! 🚀


2. Use modern image formats:
==============================
// WebP is ~30% smaller than JPEG with same quality!
<img src="restaurant.webp" width="800" height="600" alt="Restaurant" />

// Always set width and height! (also fixes CLS — see below)


3. Use a CDN:
==============
Serve your content from a server that's CLOSE to the user.
User in Mumbai → serve from Mumbai CDN node, not a US server!
Vercel, Netlify, Cloudflare do this automatically.

How to improve INP (make clicks feel snappy):

1. Use startTransition for non-urgent updates:
================================================
import { useTransition } from "react";

const [isPending, startTransition] = useTransition();

function handleSearch(value) {
    setInputValue(value); // urgent — show what user typed immediately!

    startTransition(() => {
        // non-urgent — filtering can wait a bit
        setFilteredItems(items.filter(i => i.name.includes(value)));
    });
}

// User types → input updates immediately (no lag!)
// Filter results update a moment later (acceptable!)
// Without startTransition: both updates fight → input feels laggy!


2. Debounce expensive event handlers:
=======================================
// Don't filter 1000 items on EVERY keystroke!
const handleSearch = useMemo(
    () => debounce((value) => {
        setFilteredItems(items.filter(i => i.name.includes(value)));
    }, 150),  // wait 150ms after user stops typing
    [items]
);

// User types "pizza" → filter runs ONCE after they stop, not on every letter!

How to improve CLS (stop the page from jumping):

1. ALWAYS set width and height on images:
==========================================
// ❌ Without dimensions — browser doesn't know how much space to reserve!
<img src="restaurant.webp" alt="Restaurant" />
// Image loads → page jumps! CLS goes up.

// ✅ With dimensions — browser reserves the exact space upfront!
<img src="restaurant.webp" width="800" height="400" alt="Restaurant" />
// No jump when image loads!


2. Reserve space for dynamic content:
=======================================
.ad-banner {
    min-height: 90px;  /* reserve space before ad loads */
}
/* Ad loads → fills the reserved space → no jump! */


3. Don't insert content ABOVE existing content!
=================================================
// ❌ Adding a cookie banner at the TOP pushes everything down → huge CLS!
// ✅ Show it at the BOTTOM, or as an overlay
// ✅ Or reserve its space with CSS upfront

Performance Optimization Checklist

When your app is slow, go through this list:
=============================================

STEP 1: MEASURE
  □ Open React DevTools Profiler
  □ Record a slow interaction
  □ Find the yellow/slow components
  □ Check WHY they re-rendered

STEP 2: FIX RE-RENDER PROBLEMS
  □ Is a component re-rendering because its parent does?
    → Try children-as-props pattern OR React.memo
  □ Is an object/array created inline in render?
    → Move it outside OR use useMemo for stable reference
  □ Is a callback passed as prop to memo'd component?
    → Wrap it with useCallback

STEP 3: FIX SLOW LISTS
  □ More than 100 items in a list?
    → Add react-window virtualization

STEP 4: FIX BUNDLE SIZE
  □ Large components not needed on first load?
    → React.lazy + Suspense
  □ Images below the fold?
    → Add loading="lazy" attribute

STEP 5: FIX WEB VITALS
  □ LCP slow? → Preload hero image, use WebP, use CDN
  □ INP slow? → startTransition, debounce event handlers
  □ CLS bad?  → Add width/height to images, reserve space for dynamic content

Interview Questions

Trust me, these are the most common React performance interview questions!

Q: How do you find and fix performance problems in a React app?

"I always measure first before optimizing — I use React DevTools Profiler to record interactions and find components that take too long or re-render too often. For Web Vitals, I use Lighthouse in Chrome DevTools. Once I find the bottleneck, I fix it specifically: if a component re-renders unnecessarily, I use React.memo or the children-as-props pattern. If a calculation is expensive, I use useMemo. For big lists, I add virtualization with react-window. The key is: profile first, fix the actual problem, then verify it improved."

Q: What is React.memo and when should you use it?

"React.memo is a higher-order component that wraps a component and skips re-rendering if the props haven't changed. It uses shallow comparison on each prop. You should use it when a component is expensive to render AND its parent re-renders frequently with the same props. The common pitfalls are passing inline objects or functions as props — these look different on every render because they're new references, so memo never helps. You'd pair React.memo with useMemo for stable object props and useCallback for stable function props."

Q: Explain virtualization. When would you use it?

"Virtualization means only rendering the items visible in the viewport instead of all items at once. With 10,000 items, you'd have 10,000 DOM nodes — the browser struggles to layout and paint that many nodes. react-window solves this by rendering only about 10-15 visible items at a time, recycling DOM nodes as the user scrolls. I'd use it when a list has more than 100-500 items. For smaller lists, the added complexity isn't worth it — just render them all."

Q: What are Web Vitals and how do you improve them?

"Web Vitals are Google's metrics for measuring real user experience — they also affect SEO rankings. The three main ones are: LCP (Largest Contentful Paint) — how fast the main content loads, target under 2.5 seconds, improved by preloading hero images, using WebP format, and CDN; INP (Interaction to Next Paint) — how fast the page responds to clicks, target under 200ms, improved with startTransition and debouncing; and CLS (Cumulative Layout Shift) — whether the page jumps around, target under 0.1, fixed by always setting width and height on images and reserving space for dynamic content."

Q: What is the children-as-props pattern?

"When a parent component has its own state and also renders expensive children, those children re-render every time the parent's state changes. Instead of wrapping children in React.memo, you can lift the stateful logic into a wrapper component that accepts children as a prop. The expensive component is passed as children from a higher ancestor that doesn't re-render. When the wrapper's state updates, its children prop reference hasn't changed — React skips re-rendering the expensive component entirely. It's an elegant structural solution that avoids memoization complexity."

Key Points to Remember

TechniqueIn Simple WordsUse When
React ProfilerFind which components are slow and whyAlways measure FIRST before optimizing!
React.memoSkip re-render if props haven't changedExpensive component with frequently re-rendering parent
useMemoCache an expensive calculated valueHeavy computation like sorting 1000+ items
useCallbackCache a function referencePassing function to React.memo'd child component
Children as propsPass expensive children from outside stateful componentElegant alternative to React.memo — no overhead!
VirtualizationOnly render visible items in a listLists with 100+ items (use react-window)
Lazy imagesOnly load images when near viewportAny image below the fold
Code splittingOnly download code when it's neededHeavy components, modals, route pages
LCP fixPreload hero image, use WebP, CDNMain content loads slower than 2.5 seconds
INP fixstartTransition + debounce clicksClicks or typing feel laggy
CLS fixSet image dimensions, reserve spacePage content jumps while loading

Congratulations — All 4 Deep Dives Complete!

You have finished ALL 4 Deep Dives of Namaste React!

You now understand React at a level most developers never reach:

  • Deep Dive 01 — Virtual DOM, Fiber, Reconciliation, Concurrent Mode
  • Deep Dive 02 — How hooks work internally, Rules of Hooks, building useState from scratch
  • Deep Dive 03 — React Server Components, RSC vs SSR, Streaming
  • Deep Dive 04 — Performance profiling, memoization, virtualization, Web Vitals

You are now ready to ace any React interview and build fast, production-grade applications!

Keep coding, keep learning! See you in the next course! 🚀