Skip to main content

🧱 Part 14 — Performance Tuning (memo, useCallback, Profiler, Virtualization) ⚡️


Make your app feel snappy by avoiding unnecessary work. This part is practical, beginner‑friendly, and packed with commented examples you can paste in.


🎛️ The Big Ideas

  • Render less: memoize components and values so React doesn’t re-render needlessly.
  • Do less: avoid heavy computations during render; cache them with useMemo.
  • Work smarter: virtualize long lists so the DOM only contains visible rows.
  • Measure first: use the React Profiler (and browser Performance tab) to verify improvements.

Golden rule: Measure → Change → Measure again.


1) React.memo — skip re-renders when props didn’t change

// src/components/perf/ExpensiveRow.tsx
import React from 'react';

type RowProps = {
  id: string;
  title: string;
  done: boolean;
  onToggle: (id: string) => void;
};

function RowBase({ id, title, done, onToggle }: RowProps) {
  // pretend this is heavy (expensive formatting, etc.)
  for (let i = 0; i < 200_000; i++); // demo CPU work

  return (
    <div className="flex items-center gap-2 py-1">
      <input type="checkbox" checked={done} onChange={() => onToggle(id)} />
      <span className={done ? 'line-through text-gray-500' : ''}>{title}</span>
    </div>
  );
}

// Wrap with React.memo so the row only re-renders when props change
export const ExpensiveRow = React.memo(RowBase);

Why it helps: If the parent re-renders but a row’s props are identical (by shallow compare), React skips that row.


2) useCallback — keep handler references stable

Without stable callbacks, memoized children still re-render because prop references change.

// src/components/perf/TaskListMemo.tsx
import { useCallback, useState } from 'react';
import { ExpensiveRow } from './ExpensiveRow';

type Task = { id: string; title: string; done: boolean };

export default function TaskListMemo() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: '1', title: 'Write docs', done: false },
    { id: '2', title: 'Polish UI', done: true },
  ]);

  // Stable reference: only changes if setTasks changes (it won’t)
  const onToggle = useCallback((id: string) => {
    setTasks((prev) => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }, []);

  return (
    <div>
      {tasks.map(t => (
        <ExpensiveRow key={t.id} id={t.id} title={t.title} done={t.done} onToggle={onToggle} />
      ))}
    </div>
  );
}

Tip: Only add dependencies to useCallback that can change; empty [] is fine for stable setters like setState.


3) useMemo — cache heavy computed values

// src/components/perf/HeavyStats.tsx
import { useMemo } from 'react';

type Props = { items: number[] };

export function HeavyStats({ items }: Props) {
  // Expensive calc (e.g., large aggregation)
  const { sum, avg } = useMemo(() => {
    let s = 0;
    for (let i = 0; i < items.length; i++) s += items[i];
    const avg = items.length ? s / items.length : 0;
    return { sum: s, avg };
  }, [items]);

  return (
    <div className="text-sm">
      <div>Sum: {sum}</div>
      <div>Avg: {avg.toFixed(2)}</div>
    </div>
  );
}

Guideline: Memoize only if the computation is non‑trivial or the object/array identity churn causes re-renders downstream.


4) Derived state vs real state

Avoid storing derived values (e.g., filtered = items.filter(...)) in state. Compute them on render or with useMemo.

// ❌ Avoid this
// const [filtered, setFiltered] = useState<Task[]>([]);
// setFiltered(items.filter(...))

// ✅ Prefer derived
// const filtered = useMemo(() => items.filter(...), [items]);

5) Keying lists correctly

  • Use stable IDs for key, not indexes (index keys break when you reorder/insert).
  • Keys help React reconcile efficiently → fewer DOM ops.
{tasks.map((t) => (
  <ExpensiveRow key={t.id} id={t.id} title={t.title} done={t.done} onToggle={onToggle} />
))}

6) Virtualize long lists (render only visible rows)

For thousands of items, even memoization isn’t enough. Virtualization only mounts what’s on screen.

pnpm add react-virtuoso
// src/components/perf/TaskListVirtual.tsx
import { Virtuoso } from 'react-virtuoso';

type Task = { id: string; title: string; done: boolean };

type Props = { items: Task[]; onToggle: (id: string) => void };

export function TaskListVirtual({ items, onToggle }: Props) {
  return (
    <Virtuoso
      totalCount={items.length}
      itemContent={(index) => {
        const t = items[index];
        return (
          <div className="px-3 py-2 border-b">
            <input type="checkbox" checked={t.done} onChange={() => onToggle(t.id)} />
            <span className="ml-2">{t.title}</span>
          </div>
        );
      }}
      style={{ height: 400 }}
    />
  );
}

Notes: Virtualization libraries (react‑virtuoso, react‑window, react‑virtualized) handle item measurement, windowing, and smooth scrolling.


7) Memoized selectors (when using Redux)

If you compute derived data from the store, use createSelector to avoid recalculation unless inputs change.

// src/store/selectors.ts
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '@/store';

export const selectTasks = (s: RootState) => (s as any).tasks?.items ?? [];
export const selectSearch = (s: RootState) => s.ui.filter.search;

export const selectVisibleTasks = createSelector(
  [selectTasks, selectSearch],
  (tasks, search) => tasks.filter((t: any) => t.title.toLowerCase().includes(search.toLowerCase())),
);

8) Server-state tuning (when using TanStack Query)

  • Increase staleTime for data that rarely changes to reduce refetches.
  • Use select in queries to compute once and cache the projection.
useQuery({
  queryKey: ['tasks', page],
  queryFn: fetchTasks,
  staleTime: 60_000,                // 1 minute fresh
  select: (data) => data.items,     // cache just what you need
});

9) Code splitting & Suspense (defer work)

Load heavy pages/components lazily so initial render is fast.

import { lazy, Suspense } from 'react';
const Settings = lazy(() => import('@/pages/settings/Settings'));

export function Routes() {
  return (
    <Suspense fallback={<p className="p-4">Loading…</p>}>
      <Settings />
    </Suspense>
  );
}

10) Measure: React Profiler (DevTools)

  1. Open React DevTools → Profiler tab.
  2. Click Start profiling → interact → Stop.
  3. Look for components with big render times or many commits.
  4. Add memo, useCallback, useMemo, or virtualization where it matters.

You can also wrap code with the <Profiler> API to log:

import { Profiler } from 'react';

function onRender(id: string, phase: 'mount' | 'update', actualDuration: number) {
  // send to analytics or console
  console.log(`[${id}] ${phase} in ${actualDuration.toFixed(2)}ms`);
}

export function ProfiledList() {
  return (
    <Profiler id="TaskList" onRender={onRender}>
      {/* children */}
    </Profiler>
  );
}

11) Common pitfalls (and quick fixes)

  • Unstable object/array props → move them out or memoize:
    // ❌ inline each render
    <Child options={{ dense: true }} />
    // ✅ stable
    const options = useMemo(() => ({ dense: true }), []);
    <Child options={options} />
    
  • Large lists without virtualization → adopt react-virtuoso or react-window.
  • Expensive work in render → push into useMemo/useEffect or the server.
  • Over‑memoization → memo adds overhead; use where it actually helps (after profiling).

✅ Wrap‑Up

You learned how to skip unnecessary renders (memo, useCallback), cache heavy work (useMemo), virtualize lists, and measure with the Profiler. Apply these where profiling shows hotspots, and your UI will feel instantly more responsive.

Part 14 is now on your canvas only. It’s a practical, beginner-friendly performance guide with:

  • React.memo, useCallback, useMemo
  • List keying best practices
  • Virtualization via react-virtuoso
  • Memoized Redux selectors and React Query tuning
  • Code-splitting with lazy/Suspense
  • Profiler usage and common pitfalls


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...