Fossils⚛ïļ React PatternsPerformance Optimization Interview Questions
ðŸĶ–DinosaurReactPerformanceMemoizationVirtualizationReact.memo

Performance Optimization Interview Questions

Re-renders, memoization, virtualization — the performance questions that separate juniors from seniors.

Performance Optimization Interview Questions

Performance questions are where interviewers find out if you've actually shipped production apps. Anyone can recite the API — these questions test whether you know when and why to use each tool.


Q8: How Do You Prevent Unnecessary Re-renders?

Interview Question: "A component is re-rendering too often. How do you fix it?"

The Simple Explanation

Think of React like a domino chain. When a parent component re-renders, every child re-renders too — even if the child's props didn't change. The dominoes just keep falling.

Most of the time, this is fine. React is fast. But if you have a component that's expensive to render (a chart, a huge list, a complex form), those unnecessary re-renders start adding up.

There are three tools to stop the dominoes: React.memo, useMemo, and useCallback. But the senior move is to fix the architecture first so you don't need them.

Step 1: Fix the Architecture (Free Optimization)

Before reaching for memoization, try these zero-cost fixes:

Move state down — co-locate state with the component that actually uses it.

// BAD: Everyone re-renders when hoveredId changes
function ProductPage() {
  const [hoveredId, setHoveredId] = useState(null);
  return (
    <>
      <ExpensiveHeader />
      <ProductGrid onHover={setHoveredId} hoveredId={hoveredId} />
      <ExpensiveFooter />
    </>
  );
}
 
// GOOD: Only ProductGrid cares about hover state
function ProductPage() {
  return (
    <>
      <ExpensiveHeader />
      <ProductGrid />
      <ExpensiveFooter />
    </>
  );
}
 
function ProductGrid() {
  const [hoveredId, setHoveredId] = useState(null);
  // Only this component re-renders on hover
  return /* ... */;
}

Lift content up — pass expensive children as children prop so they don't re-render.

function ScrollContainer({ children }) {
  const [scrollY, setScrollY] = useState(0);
  useEffect(() => {
    const handler = () => setScrollY(window.scrollY);
    window.addEventListener('scroll', handler);
    return () => window.removeEventListener('scroll', handler);
  }, []);
 
  return (
    <div>
      <ScrollBar position={scrollY} />
      {children} {/* These don't re-render! */}
    </div>
  );
}

Why? Because children was created by the parent. When scrollY changes and ScrollContainer re-renders, the children JSX element is the same reference — React skips re-rendering it.

Step 2: React.memo — Stop the Dominoes

React.memo wraps a component and says: "Only re-render if the props actually changed."

const ProductCard = React.memo(function ProductCard({ name, price, image }) {
  console.log('Rendering ProductCard'); // Only logs when props change
  return (
    <div>
      <img src={image} alt={name} />
      <h3>{name}</h3>
      <p>${price}</p>
    </div>
  );
});

But there's a catch. React.memo compares props using shallow equality (===). If the parent passes a new object or function reference every render, the memo is useless.

Step 3: useCallback and useMemo — Stabilize Props

function ProductPage({ products }) {
  // Without useCallback, this is a new function every render
  // → React.memo on ProductCard would be bypassed
  const handleAddToCart = useCallback((productId: string) => {
    addToCart(productId);
  }, []);
 
  // Without useMemo, this is a new array every render
  const sortedProducts = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );
 
  return sortedProducts.map(product => (
    <ProductCard
      key={product.id}
      {...product}
      onAddToCart={handleAddToCart}
    />
  ));
}

Think of useCallback as saying "this is the same function as last time" and useMemo as "this is the same computed value as last time."

The Optimization Decision Tree

  1. Is the component actually slow? Measure first with React DevTools Profiler.
  2. Can you move state down or use children-as-props? Do that first — it's free.
  3. Is a child expensive and receiving stable props? Use React.memo.
  4. Are you passing objects/functions that break memo? Use useMemo/useCallback.

The One-Liner That Impresses

"The best re-render optimization is architectural — co-locate state and use composition so re-renders are naturally scoped. React.memo, useMemo, and useCallback are a second line of defense for when a component is provably expensive and its parent re-renders frequently with unchanged data."

Common Follow-Up Questions

"Should you memoize everything by default?"

No. Memoization has a cost — memory for the cached value, comparison cost on every render, and code complexity. For cheap components, the overhead of memoization can actually be more expensive than just re-rendering. Only memoize when you've measured a real problem.

"How do you know if a component is re-rendering unnecessarily?"

Use React DevTools Profiler. It shows you which components rendered, how long they took, and why they rendered. The "Highlight Updates" feature visually flashes components as they re-render.

Red Flags in Your Answer

  • Reaching for React.memo before considering architecture
  • Saying "I memoize everything just in case"
  • Not knowing that React.memo does shallow comparison
  • Unable to explain why passing () => {} inline breaks memoization
  • Not mentioning the React DevTools Profiler

Q9: What Is Memoization?

Interview Question: "Explain memoization. When should and shouldn't you use it in React?"

The Simple Explanation

Think of memoization like a cheat sheet for a math test. The first time you solve 47 × 83, you do the full calculation and write the answer on your cheat sheet. The next time you see 47 × 83, you just look at the cheat sheet instead of recalculating.

Memoization means caching the result of an expensive computation so you can return the cached result when the same inputs appear again, instead of recomputing from scratch.

Memoization in React

React gives you two memoization tools:

useMemo — cache a computed value

function AnalyticsDashboard({ transactions }) {
  // Expensive: sorts and aggregates thousands of transactions
  const summary = useMemo(() => {
    const sorted = [...transactions].sort((a, b) => b.amount - a.amount);
    const total = sorted.reduce((sum, t) => sum + t.amount, 0);
    const average = total / sorted.length;
    return { sorted, total, average };
  }, [transactions]); // Only recompute when transactions change
 
  return (
    <div>
      <p>Total: ${summary.total}</p>
      <p>Average: ${summary.average}</p>
      <TransactionList items={summary.sorted} />
    </div>
  );
}

React.memo — cache a component's rendered output

const Chart = React.memo(function Chart({ data, width, height }) {
  // Expensive: renders an SVG chart with thousands of data points
  return (
    <svg width={width} height={height}>
      {data.map(point => (
        <circle key={point.id} cx={point.x} cy={point.y} r={3} />
      ))}
    </svg>
  );
});

When NOT to Use Memoization

This is where interviewers test your depth. Memoization is not free:

// WASTEFUL: the computation is trivial
const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);
// Just write: const fullName = `${firstName} ${lastName}`;
 
// WASTEFUL: new object every render anyway
const style = useMemo(
  () => ({ color: darkMode ? 'white' : 'black' }),
  [darkMode]
);
// The useMemo overhead exceeds the savings for such a simple object
 
// WASTEFUL: deps change every render, so it recomputes anyway
const filtered = useMemo(
  () => items.filter(predicate),
  [items, predicate] // If predicate is a new function each render, useMemo does nothing
);

The cost of memoization:

  • Memory to store the cached value
  • Comparison cost to check if deps changed (every render)
  • Code complexity and cognitive overhead
  • False sense of optimization

The Decision: To Memoize or Not?

SituationMemoize?
Sorting/filtering thousands of itemsYes
String concatenationNo
Creating a simple objectNo
Heavy SVG/Canvas renderingYes
Computing derived data from large datasetsYes
The component renders fast alreadyNo
Deps change every render anywayNo (useless)

The One-Liner That Impresses

"Memoization trades memory for speed by caching computation results — but in React, it only helps when the computation is genuinely expensive AND the dependencies are stable. Memoizing cheap operations adds overhead without benefit, and memoizing with unstable dependencies gives you the worst of both worlds: overhead with no caching."

Common Follow-Up Questions

"How does React decide if dependencies changed?"

Object.is comparison (essentially ===). Primitives are compared by value, objects and arrays by reference. This means [1, 2, 3] !== [1, 2, 3] — same content, different reference — so creating new arrays/objects in the parent defeats child memoization.

"What about the React Compiler?"

The React Compiler (formerly React Forget) auto-memoizes components and hooks at build time. When it ships broadly, you'll write less manual useMemo/useCallback. But understanding when memoization helps vs. hurts is still essential — the compiler can't fix bad architecture.

Red Flags in Your Answer

  • Defining memoization but not being able to say when it's harmful
  • Memoizing trivial computations (string concatenation, simple math)
  • Not knowing that unstable dependencies defeat the purpose
  • Saying "always use useMemo for performance" without measuring
  • Forgetting that React.memo is shallow comparison

Q10: What Is Virtualization?

Interview Question: "You have a list of 10,000 items. How do you render it without killing performance?"

The Simple Explanation

Think of it like a window on a train. You're passing through a city with thousands of buildings, but you can only see maybe 10-15 buildings through your window at any time. Your brain doesn't need to "render" all the buildings in the city — just the ones visible through the window.

Virtualization works the same way. Instead of rendering 10,000 DOM nodes (which would freeze the browser), you only render the ~20-30 items visible in the scroll viewport. As the user scrolls, items entering the viewport are created and items leaving are destroyed.

How It Works

Full list: [item1] [item2] [item3] [item4] [item5] ... [item10000]
 
What the DOM actually contains:
┌─────────────────────────────┐
│  spacer div (height: 5000px)│  ← empty space for items above
├─────────────────────────────â”Ī
│  [item 501]                 │  ← only visible items
│  [item 502]                 │     are real DOM nodes
│  [item 503]                 │
│  [item 504]                 │
│  ...                        │
│  [item 520]                 │
├─────────────────────────────â”Ī
│  spacer div (height: 45000) │  ← empty space for items below
└─────────────────────────────┘

The container has the correct total scroll height (so the scrollbar looks right), but only ~20 real DOM elements exist at any time.

Implementation with react-window

import { FixedSizeList } from 'react-window';
 
function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name} — ${items[index].price}
    </div>
  );
 
  return (
    <FixedSizeList
      height={600}       // viewport height
      width="100%"
      itemCount={items.length}
      itemSize={50}      // each row is 50px tall
    >
      {Row}
    </FixedSizeList>
  );
}

For variable-height rows, use VariableSizeList and provide a function that returns each item's height.

Virtualization + Infinite Scroll

This is the real-world pattern interviewers want to see — combining virtualization with paginated data loading:

function InfiniteProductList() {
  const [items, setItems] = useState<Product[]>([]);
  const isFetching = useRef(false);
  const hasMore = useRef(true);
  const page = useRef(1);
 
  const loadMore = useCallback(async () => {
    if (isFetching.current || !hasMore.current) return;
 
    isFetching.current = true;
    const newItems = await fetchProducts(page.current);
 
    if (newItems.length === 0) {
      hasMore.current = false;
    } else {
      setItems(prev => [...prev, ...newItems]);
      page.current += 1;
    }
    isFetching.current = false;
  }, []);
 
  const handleItemsRendered = useCallback(
    ({ visibleStopIndex }: { visibleStopIndex: number }) => {
      // Load more when user scrolls near the end
      if (visibleStopIndex > items.length - 10) {
        loadMore();
      }
    },
    [items.length, loadMore]
  );
 
  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={hasMore.current ? items.length + 1 : items.length}
      itemSize={80}
      onItemsRendered={handleItemsRendered}
    >
      {({ index, style }) => {
        if (index >= items.length) {
          return <div style={style}>Loading more...</div>;
        }
        return (
          <div style={style}>
            <ProductCard product={items[index]} />
          </div>
        );
      }}
    </FixedSizeList>
  );
}

Notice the refs for isFetching and hasMore — these are guard values that need to be read instantly and shouldn't trigger re-renders (see Q6 on useRef).

Why Not Just Use CSS overflow?

CSS overflow: auto with all 10,000 items still creates 10,000 DOM nodes. The browser has to:

  • Parse 10,000 elements
  • Calculate layout for 10,000 elements
  • Hold 10,000 elements in memory

Virtualization keeps the DOM node count at ~20-30 regardless of list size. The difference: a 10,000-item list might use 500MB of memory without virtualization vs. 2MB with it.

Popular Libraries

LibraryBest For
react-windowSimple lists and grids, lightweight
react-virtuosoVariable heights, grouped lists, auto-sizing
@tanstack/react-virtualHeadless (no built-in UI), maximum flexibility

The One-Liner That Impresses

"Virtualization only renders the items visible in the viewport — instead of 10,000 DOM nodes, you maintain ~20-30 and swap them as the user scrolls. Combined with infinite scroll using refs for fetch guards and intersection or scroll-position triggers, you get a list that handles millions of items with constant memory usage."

Common Follow-Up Questions

"What about search and keyboard navigation in a virtualized list?"

You need to manage scroll-to-index behavior. Most virtualization libraries expose a scrollToItem(index) method. For search, find the matching index in your data array and programmatically scroll to it.

"When should you NOT virtualize?"

When the list is small (under ~100 items), when items have wildly variable heights that are expensive to measure, or when SEO matters (virtualized items aren't in the DOM for crawlers). For SEO-critical lists, render the full HTML on the server and virtualize on the client.

Red Flags in Your Answer

  • Saying "just use pagination" without addressing the infinite scroll use case
  • Not knowing any virtualization library
  • Describing virtualization without mentioning the spacer/offset technique
  • Not connecting it to real-world patterns like infinite scroll
  • Forgetting about the memory and DOM node count benefits